From 3d537c9f99e4e20504ef3d5040d845a822c1ee20 Mon Sep 17 00:00:00 2001 From: huiwq1990 Date: Thu, 18 May 2023 15:45:06 +0800 Subject: [PATCH 01/93] upgrade chart version (#1469) --- charts/pool-coordinator/Chart.yaml | 2 +- charts/yurt-manager/Chart.yaml | 2 +- charts/yurthub/Chart.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/pool-coordinator/Chart.yaml b/charts/pool-coordinator/Chart.yaml index 5d4faf3ba2d..bd69b523971 100644 --- a/charts/pool-coordinator/Chart.yaml +++ b/charts/pool-coordinator/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.0 +version: 1.3.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/yurt-manager/Chart.yaml b/charts/yurt-manager/Chart.yaml index 8c80c03a2a5..74238dda7b3 100644 --- a/charts/yurt-manager/Chart.yaml +++ b/charts/yurt-manager/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.0 +version: 1.3.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/yurthub/Chart.yaml b/charts/yurthub/Chart.yaml index c678bd3ab75..b651e51caed 100644 --- a/charts/yurthub/Chart.yaml +++ b/charts/yurthub/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.0 +version: 1.3.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to From 505c27acb43d8d5cfe50cca4d0b57f65a1d8ba6f Mon Sep 17 00:00:00 2001 From: lanyujie <80832542+yojay11717@users.noreply.github.com> Date: Thu, 18 May 2023 16:48:06 +0800 Subject: [PATCH 02/93] optimization: replacing commonly used HTTP strings with enumerations. (#1465) --- pkg/util/util.go | 16 ++++++++++++++++ pkg/yurthub/otaupdate/util/util.go | 3 ++- pkg/yurthub/proxy/local/faketoken.go | 3 ++- pkg/yurthub/proxy/local/local.go | 9 +++++---- pkg/yurthub/proxy/pool/pool.go | 5 +++-- pkg/yurthub/proxy/remote/loadbalancer.go | 5 +++-- pkg/yurthub/server/certificate.go | 3 ++- pkg/yurthub/server/nonresource.go | 3 ++- 8 files changed, 35 insertions(+), 12 deletions(-) diff --git a/pkg/util/util.go b/pkg/util/util.go index bcf3bc9d74f..8afc0e3816a 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -29,3 +29,19 @@ func IsNil(i interface{}) bool { } return false } + +const ( + // HttpHeaderContentType HTTP request header keyword: Content-Type which is used in HTTP request and response + // headers to specify the media type of the entity body + HttpHeaderContentType = "Content-Type" + // HttpHeaderContentLength HTTP request header keyword: Content-Length which is used to indicate the size of the + // message body, ensuring that the message can be transmitted and parsed correctly + HttpHeaderContentLength = "Content-Length" + // HttpHeaderTransferEncoding HTTP request header keyword: Transfer-Encoding which is used to indicate the HTTP + // transmission encoding type used by the server + HttpHeaderTransferEncoding = "Transfer-Encoding" + + // HttpContentTypeJson HTTP request Content-Type type: application/json which is used to indicate that the data + // type transmitted in the HTTP request and response body is JSON + HttpContentTypeJson = "application/json" +) diff --git a/pkg/yurthub/otaupdate/util/util.go b/pkg/yurthub/otaupdate/util/util.go index e1c67cc41e4..c9481013819 100644 --- a/pkg/yurthub/otaupdate/util/util.go +++ b/pkg/yurthub/otaupdate/util/util.go @@ -28,6 +28,7 @@ import ( "k8s.io/klog/v2" "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater" + yurtutil "github.com/openyurtio/openyurt/pkg/util" ) // Derived from kubelet encodePods @@ -52,7 +53,7 @@ func WriteJSONResponse(w http.ResponseWriter, data []byte) { w.WriteHeader(http.StatusOK) return } - w.Header().Set("Content-Type", "application/json") + w.Header().Set(yurtutil.HttpHeaderContentType, yurtutil.HttpContentTypeJson) w.WriteHeader(http.StatusOK) n, err := w.Write(data) if err != nil || n != len(data) { diff --git a/pkg/yurthub/proxy/local/faketoken.go b/pkg/yurthub/proxy/local/faketoken.go index 7837a0fa70d..893f6cdbb4a 100644 --- a/pkg/yurthub/proxy/local/faketoken.go +++ b/pkg/yurthub/proxy/local/faketoken.go @@ -31,6 +31,7 @@ import ( apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/klog/v2" + yurtutil "github.com/openyurtio/openyurt/pkg/util" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer" "github.com/openyurtio/openyurt/pkg/yurthub/util" ) @@ -43,7 +44,7 @@ func WithFakeTokenInject(handler http.Handler, serializerManager *serializer.Ser if info.Resource == "serviceaccounts" && info.Subresource == "token" { klog.Infof("find serviceaccounts token request when cluster is unhealthy, try to write fake token to response.") var buf bytes.Buffer - headerNStr := req.Header.Get("Content-Length") + headerNStr := req.Header.Get(yurtutil.HttpHeaderContentLength) headerN, _ := strconv.Atoi(headerNStr) n, err := buf.ReadFrom(req.Body) if err != nil || (headerN != 0 && int(n) != headerN) { diff --git a/pkg/yurthub/proxy/local/local.go b/pkg/yurthub/proxy/local/local.go index db61629351c..46b7fb30c9f 100644 --- a/pkg/yurthub/proxy/local/local.go +++ b/pkg/yurthub/proxy/local/local.go @@ -34,6 +34,7 @@ import ( apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/klog/v2" + yurtutil "github.com/openyurtio/openyurt/pkg/util" manager "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" hubmeta "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/meta" "github.com/openyurtio/openyurt/pkg/yurthub/proxy/util" @@ -132,7 +133,7 @@ func (lp *LocalProxy) localPost(w http.ResponseWriter, req *http.Request) error req.Body = rc } - headerNStr := req.Header.Get("Content-Length") + headerNStr := req.Header.Get(yurtutil.HttpHeaderContentLength) headerN, _ := strconv.Atoi(headerNStr) n, err := buf.ReadFrom(req.Body) if err != nil || (headerN != 0 && int(n) != headerN) { @@ -171,8 +172,8 @@ func (lp *LocalProxy) localWatch(w http.ResponseWriter, req *http.Request) error ctx := req.Context() contentType, _ := hubutil.ReqContentTypeFrom(ctx) - w.Header().Set("Content-Type", contentType) - w.Header().Set("Transfer-Encoding", "chunked") + w.Header().Set(yurtutil.HttpHeaderContentType, contentType) + w.Header().Set(yurtutil.HttpHeaderTransferEncoding, "chunked") w.WriteHeader(http.StatusOK) flusher.Flush() @@ -236,7 +237,7 @@ func (lp *LocalProxy) localReqCache(w http.ResponseWriter, req *http.Request) er func copyHeader(dst, src http.Header) { for k, vv := range src { - if k == "Content-Type" || k == "Content-Length" { + if k == yurtutil.HttpHeaderContentType || k == yurtutil.HttpHeaderContentLength { for _, v := range vv { dst.Add(k, v) } diff --git a/pkg/yurthub/proxy/pool/pool.go b/pkg/yurthub/proxy/pool/pool.go index eee8a7f7db3..e631d475f8a 100644 --- a/pkg/yurthub/proxy/pool/pool.go +++ b/pkg/yurthub/proxy/pool/pool.go @@ -28,6 +28,7 @@ import ( apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/klog/v2" + yurtutil "github.com/openyurtio/openyurt/pkg/util" "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/filter/manager" "github.com/openyurtio/openyurt/pkg/yurthub/proxy/util" @@ -224,7 +225,7 @@ func (pp *PoolCoordinatorProxy) modifyResponse(resp *http.Response) error { if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { // prepare response content type reqContentType, _ := hubutil.ReqContentTypeFrom(ctx) - respContentType := resp.Header.Get("Content-Type") + respContentType := resp.Header.Get(yurtutil.HttpHeaderContentType) if len(respContentType) == 0 { respContentType = reqContentType } @@ -243,7 +244,7 @@ func (pp *PoolCoordinatorProxy) modifyResponse(resp *http.Response) error { resp.Body = filterRc if size > 0 { resp.ContentLength = int64(size) - resp.Header.Set("Content-Length", fmt.Sprint(size)) + resp.Header.Set(yurtutil.HttpHeaderContentLength, fmt.Sprint(size)) } // after gunzip in filter, the header content encoding should be removed. diff --git a/pkg/yurthub/proxy/remote/loadbalancer.go b/pkg/yurthub/proxy/remote/loadbalancer.go index 923ab0bd7cf..a040dea5124 100644 --- a/pkg/yurthub/proxy/remote/loadbalancer.go +++ b/pkg/yurthub/proxy/remote/loadbalancer.go @@ -29,6 +29,7 @@ import ( apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/klog/v2" + yurtutil "github.com/openyurtio/openyurt/pkg/util" "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/filter/manager" "github.com/openyurtio/openyurt/pkg/yurthub/healthchecker" @@ -278,7 +279,7 @@ func (lb *loadBalancer) modifyResponse(resp *http.Response) error { if resp.StatusCode >= http.StatusOK && resp.StatusCode <= http.StatusPartialContent { // prepare response content type reqContentType, _ := hubutil.ReqContentTypeFrom(ctx) - respContentType := resp.Header.Get("Content-Type") + respContentType := resp.Header.Get(yurtutil.HttpHeaderContentType) if len(respContentType) == 0 { respContentType = reqContentType } @@ -297,7 +298,7 @@ func (lb *loadBalancer) modifyResponse(resp *http.Response) error { resp.Body = filterRc if size > 0 { resp.ContentLength = int64(size) - resp.Header.Set("Content-Length", fmt.Sprint(size)) + resp.Header.Set(yurtutil.HttpHeaderContentLength, fmt.Sprint(size)) } // after gunzip in filter, the header content encoding should be removed. diff --git a/pkg/yurthub/server/certificate.go b/pkg/yurthub/server/certificate.go index 028d88a2225..a364a830880 100644 --- a/pkg/yurthub/server/certificate.go +++ b/pkg/yurthub/server/certificate.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" + yurtutil "github.com/openyurtio/openyurt/pkg/util" "github.com/openyurtio/openyurt/pkg/yurthub/certificate" ) @@ -56,7 +57,7 @@ func updateTokenHandler(certificateMgr certificate.YurtCertificateManager) http. } w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") + w.Header().Set(yurtutil.HttpHeaderContentType, yurtutil.HttpContentTypeJson) fmt.Fprintf(w, "update bootstrap token successfully") return }) diff --git a/pkg/yurthub/server/nonresource.go b/pkg/yurthub/server/nonresource.go index 8c53c328eee..78c81a53c01 100644 --- a/pkg/yurthub/server/nonresource.go +++ b/pkg/yurthub/server/nonresource.go @@ -28,6 +28,7 @@ import ( "k8s.io/utils/pointer" "github.com/openyurtio/openyurt/cmd/yurthub/app/config" + yurtutil "github.com/openyurtio/openyurt/pkg/util" "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/rest" "github.com/openyurtio/openyurt/pkg/yurthub/storage" @@ -121,6 +122,6 @@ func writeErrResponse(path string, err error, w http.ResponseWriter) { } func writeRawJSON(output []byte, w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set(yurtutil.HttpHeaderContentType, yurtutil.HttpContentTypeJson) w.Write(output) } From b3ae2c7af4b7bf76f55e8f2c2cc9be12f849382a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E4=BC=9F=E7=85=8C?= Date: Tue, 23 May 2023 10:40:06 +0800 Subject: [PATCH 03/93] fix memory leak for yur-tunnel-server (#1471) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9a82f88752b..6d51a51ae1f 100644 --- a/go.mod +++ b/go.mod @@ -188,7 +188,7 @@ replace ( k8s.io/mount-utils => k8s.io/mount-utils v0.22.3 k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.22.3 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.22.3 - sigs.k8s.io/apiserver-network-proxy => github.com/openyurtio/apiserver-network-proxy v1.18.8 + sigs.k8s.io/apiserver-network-proxy => github.com/openyurtio/apiserver-network-proxy v0.1.0 sigs.k8s.io/apiserver-network-proxy/konnectivity-client => sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22 ) diff --git a/go.sum b/go.sum index fb53a826fd1..619cbdee8b1 100644 --- a/go.sum +++ b/go.sum @@ -533,8 +533,8 @@ github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M5 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= -github.com/openyurtio/apiserver-network-proxy v1.18.8 h1:xXqaP8DAOvCHD7DNIqtBOhuWxCnwULLc1PqOMoJ7UeI= -github.com/openyurtio/apiserver-network-proxy v1.18.8/go.mod h1:X5Au3jBNIgYL2uK0IHeNGnZqlUlVSCFQhi/npPgkKRg= +github.com/openyurtio/apiserver-network-proxy v0.1.0 h1:uJI6LeAHmkQL0zV1+NIbgRsx2ayzsPfMA2bd1gROypc= +github.com/openyurtio/apiserver-network-proxy v0.1.0/go.mod h1:X5Au3jBNIgYL2uK0IHeNGnZqlUlVSCFQhi/npPgkKRg= github.com/openyurtio/yurt-app-manager-api v0.6.0 h1:GoayIUkdITBufJirU94dvyknFFG4On1T7XcDvsqCWaQ= github.com/openyurtio/yurt-app-manager-api v0.6.0/go.mod h1:Ql/n89HmezW7s0d2Cyq9P3hl2MEvvjjv3xxPkLVzz10= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= From 74e32829f9947bfba4acf1a2520c85761135421c Mon Sep 17 00:00:00 2001 From: rambohe Date: Wed, 24 May 2023 15:26:07 +0800 Subject: [PATCH 04/93] revert yurt-tunnel components release (#1484) --- Makefile | 9 ++++++++- .../release/Dockerfile.yurt-tunnel-agent | 14 ++++++++++++++ .../release/Dockerfile.yurt-tunnel-server | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 hack/dockerfiles/release/Dockerfile.yurt-tunnel-agent create mode 100644 hack/dockerfiles/release/Dockerfile.yurt-tunnel-server diff --git a/Makefile b/Makefile index 64b299d2dee..49fe17030f4 100644 --- a/Makefile +++ b/Makefile @@ -144,7 +144,7 @@ docker-build: # Build and Push the docker images with multi-arch -docker-push: docker-push-yurthub docker-push-node-servant docker-push-yurt-manager +docker-push: docker-push-yurthub docker-push-node-servant docker-push-yurt-manager docker-push-yurt-tunnel-server docker-push-yurt-tunnel-agent docker-buildx-builder: @@ -165,6 +165,13 @@ docker-push-node-servant: docker-buildx-builder docker-push-yurt-manager: manifests docker-buildx-builder docker buildx build --no-cache --push ${DOCKER_BUILD_ARGS} --platform ${TARGET_PLATFORMS} -f hack/dockerfiles/release/Dockerfile.yurt-manager . -t ${IMAGE_REPO}/yurt-manager:${GIT_VERSION} +docker-push-yurt-tunnel-server: docker-buildx-builder + docker buildx build --no-cache --push ${DOCKER_BUILD_ARGS} --platform ${TARGET_PLATFORMS} -f hack/dockerfiles/release/Dockerfile.yurt-tunnel-server . -t ${IMAGE_REPO}/yurt-tunnel-server:${GIT_VERSION} + +docker-push-yurt-tunnel-agent: docker-buildx-builder + docker buildx build --no-cache --push ${DOCKER_BUILD_ARGS} --platform ${TARGET_PLATFORMS} -f hack/dockerfiles/release/Dockerfile.yurt-tunnel-agent . -t ${IMAGE_REPO}/yurt-tunnel-agent:${GIT_VERSION} + + generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. # hack/make-rule/generate_openapi.sh // TODO by kadisi $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./pkg/apis/..." diff --git a/hack/dockerfiles/release/Dockerfile.yurt-tunnel-agent b/hack/dockerfiles/release/Dockerfile.yurt-tunnel-agent new file mode 100644 index 00000000000..112d192cb8f --- /dev/null +++ b/hack/dockerfiles/release/Dockerfile.yurt-tunnel-agent @@ -0,0 +1,14 @@ +# multi-arch image building for yurt-tunnel-agent + +FROM --platform=${BUILDPLATFORM} golang:1.18 as builder +ADD . /build +ARG TARGETOS TARGETARCH GIT_VERSION GOPROXY MIRROR_REPO +WORKDIR /build/ +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GIT_VERSION=${GIT_VERSION} make build WHAT=cmd/yurt-tunnel-agent + +FROM --platform=${TARGETPLATFORM} alpine:3.17 +ARG TARGETOS TARGETARCH MIRROR_REPO +RUN if [ ! -z "${MIRROR_REPO+x}" ]; then sed -i "s/dl-cdn.alpinelinux.org/${MIRROR_REPO}/g" /etc/apk/repositories; fi && \ + apk add ca-certificates bash libc6-compat && update-ca-certificates && rm /var/cache/apk/* +COPY --from=builder /build/_output/local/bin/${TARGETOS}/${TARGETARCH}/yurt-tunnel-agent /usr/local/bin/yurt-tunnel-agent +ENTRYPOINT ["/usr/local/bin/yurt-tunnel-agent"] \ No newline at end of file diff --git a/hack/dockerfiles/release/Dockerfile.yurt-tunnel-server b/hack/dockerfiles/release/Dockerfile.yurt-tunnel-server new file mode 100644 index 00000000000..860951e7b79 --- /dev/null +++ b/hack/dockerfiles/release/Dockerfile.yurt-tunnel-server @@ -0,0 +1,14 @@ +# multi-arch image building for yurt-tunnel-server + +FROM --platform=${BUILDPLATFORM} golang:1.18 as builder +ADD . /build +ARG TARGETOS TARGETARCH GIT_VERSION GOPROXY MIRROR_REPO +WORKDIR /build/ +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GIT_VERSION=${GIT_VERSION} make build WHAT=cmd/yurt-tunnel-server + +FROM --platform=${TARGETPLATFORM} alpine:3.17 +ARG TARGETOS TARGETARCH MIRROR_REPO +RUN if [ ! -z "${MIRROR_REPO+x}" ]; then sed -i "s/dl-cdn.alpinelinux.org/${MIRROR_REPO}/g" /etc/apk/repositories; fi && \ + apk add ca-certificates bash libc6-compat iptables ip6tables conntrack-tools && update-ca-certificates && rm /var/cache/apk/* +COPY --from=builder /build/_output/local/bin/${TARGETOS}/${TARGETARCH}/yurt-tunnel-server /usr/local/bin/yurt-tunnel-server +ENTRYPOINT ["/usr/local/bin/yurt-tunnel-server"] \ No newline at end of file From 6c040147b5af623783b39507ba1d78fbaed71f8d Mon Sep 17 00:00:00 2001 From: lishaokai1995 Date: Thu, 25 May 2023 09:37:07 +0800 Subject: [PATCH 05/93] Fix the grammatical errors in README.md (#1495) Signed-off-by: lishaokai1995 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5d7d5ceccb6..71b8affc667 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ English | [简体中文](./README.zh.md) OpenYurt has been designed to meet various DevOps requirements against typical edge infrastructures. It provides consistent user experience for managing the edge applications as if they were running in the cloud infrastructure. It addresses specific challenges for cloud-edge orchestration in Kubernetes such as unreliable or disconnected cloud-edge networking, -edge autonomy, edge device management, region-aware deployment and so on. OpenYurt preserves intact Kubernetes API compatibility, +edge autonomy, edge device management, region-aware deployment, and so on. OpenYurt preserves intact Kubernetes API compatibility, is vendor agnostic, and more importantly, is **SIMPLE** to use. ## Architecture @@ -47,7 +47,7 @@ multiple physical regions, which are referred to as `Pools` in OpenYurt. The above figure demonstrates the core OpenYurt architecture. The major components consist of: -- **[YurtHub](https://openyurt.io/docs/next/core-concepts/yurthub)**: YurtHub runs on worker nodes as static pod and serve as a node sidecar to handle requests that comes from components(like Kubelet, Kubeproxy, etc.) on worker nodes to kube-apiserver. +- **[YurtHub](https://openyurt.io/docs/next/core-concepts/yurthub)**: YurtHub runs on worker nodes as static pod and serves as a node sidecar to handle requests that comes from components (like Kubelet, Kubeproxy, etc.) on worker nodes to kube-apiserver. - **[Yurt-Manager](https://github.com/openyurtio/openyurt/tree/master/cmd/yurt-manager)**: include all controllers and webhooks for edge. - **[Raven-Agent](https://openyurt.io/docs/next/core-concepts/raven)**: It is focused on edge-edge and edge-cloud communication in OpenYurt, and provides layer 3 network connectivity among pods in different physical regions, as there are in one vanilla Kubernetes cluster. - **Pool-Coordinator**: One instance of Pool-Coordinator is deployed in every edge NodePool, and in conjunction with YurtHub to provide heartbeat delegation, cloud-edge traffic multiplexing abilities, etc. From 1985467e26ec0da9f03848f8dc16394c26d7b62d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 May 2023 19:14:07 +0800 Subject: [PATCH 06/93] build(deps): bump golang.org/x/sys from 0.7.0 to 0.8.0 (#1431) Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.7.0 to 0.8.0. - [Commits](https://github.com/golang/sys/compare/v0.7.0...v0.8.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6d51a51ae1f..a4a9ee5211c 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 - golang.org/x/sys v0.7.0 + golang.org/x/sys v0.8.0 google.golang.org/grpc v1.54.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/square/go-jose.v2 v2.6.0 diff --git a/go.sum b/go.sum index 619cbdee8b1..78a95f866fd 100644 --- a/go.sum +++ b/go.sum @@ -919,8 +919,8 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= From 35455535ecc0e12a45b0469a031b5cf3bb8011ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 09:48:08 +0800 Subject: [PATCH 07/93] build(deps): bump github.com/prometheus/client_golang (#1428) Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.15.0 to 1.15.1. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.15.0...v1.15.1) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a4a9ee5211c..b15bcb20f6c 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 github.com/projectcalico/api v0.0.0-20230222223746-44aa60c2201f - github.com/prometheus/client_golang v1.15.0 + github.com/prometheus/client_golang v1.15.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.2 diff --git a/go.sum b/go.sum index 78a95f866fd..0eca6ab202f 100644 --- a/go.sum +++ b/go.sum @@ -557,8 +557,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= -github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= From 1822d27c3a661a2de558b6ff77b5368cf89ace2e Mon Sep 17 00:00:00 2001 From: yeqiugt <37202250+yeqiugt@users.noreply.github.com> Date: Tue, 30 May 2023 16:02:08 +0800 Subject: [PATCH 08/93] Error strings should not be capitalized (unless beginning with proper (#1497) nouns or acronyms) or end with punctuation, since they are usually printed following other context. Signed-off-by: liulijin <253954033@qq.com> --- pkg/webhook/builder/webhook.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/webhook/builder/webhook.go b/pkg/webhook/builder/webhook.go index c8636ad0bd7..240c7694617 100644 --- a/pkg/webhook/builder/webhook.go +++ b/pkg/webhook/builder/webhook.go @@ -109,7 +109,7 @@ func (blder *WebhookBuilder) getType() (runtime.Object, error) { if blder.apiType != nil { return blder.apiType, nil } - return nil, errors.New("For() must be called with a valid object") + return nil, errors.New("for() must be called with a valid object") } // registerDefaultingWebhook registers a defaulting webhook if th. From b99c90516aabb44f41c0b4102a91f41f5fca4f00 Mon Sep 17 00:00:00 2001 From: y-ykcir Date: Fri, 2 Jun 2023 14:12:09 +0800 Subject: [PATCH 09/93] feat: add e2e test case for daemonset pod updater controller (#1504) * feat: add e2e test case for daemonset pod updater controller Signed-off-by: ricky * fix: automony failure Signed-off-by: ricky --------- Signed-off-by: ricky --- test/e2e/yurt/daemonpodupdater.go | 330 ++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 test/e2e/yurt/daemonpodupdater.go diff --git a/test/e2e/yurt/daemonpodupdater.go b/test/e2e/yurt/daemonpodupdater.go new file mode 100644 index 00000000000..e67f66ea004 --- /dev/null +++ b/test/e2e/yurt/daemonpodupdater.go @@ -0,0 +1,330 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package yurt + +import ( + "context" + "fmt" + "os/exec" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/test/e2e/util" + ycfg "github.com/openyurtio/openyurt/test/e2e/yurtconfig" +) + +const ( + PodNeedUpgrade corev1.PodConditionType = "PodNeedUpgrade" + ServerName string = "127.0.0.1" + ServerPort string = "10267" + FlannelNamespace string = "kube-flannel" +) + +var _ = Describe("daemonPodUpdater Test", Ordered, func() { + ctx := context.Background() + timeout := 60 * time.Second + k8sClient := ycfg.YurtE2eCfg.RuntimeClient + nodeToImageMap := make(map[string]string) + + var updateStrategyType string + var namespaceName string + + daemonSetName := "busybox-daemonset" + testImg1 := "busybox" + testImg2 := "busybox:1.36.0" + + createNamespace := func() { + ns := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + }, + } + Eventually( + func() error { + return k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground)) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) + By("make sure all the resources are removed") + + res := &corev1.Namespace{} + Eventually( + func() error { + return k8sClient.Get(ctx, client.ObjectKey{ + Name: namespaceName, + }, res) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(&util.NotFoundMatcher{}) + Eventually( + func() error { + return k8sClient.Create(ctx, &ns) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + } + + createDaemonSet := func() { + Eventually(func() error { + return k8sClient.Delete(ctx, &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: daemonSetName, + Namespace: namespaceName, + }, + }) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 300).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) + + testContainerName := "bs" + testLabel := map[string]string{"app": daemonSetName} + testAnnotations := map[string]string{"apps.openyurt.io/update-strategy": updateStrategyType} + + testDaemonSet := &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: daemonSetName, + Namespace: namespaceName, + Annotations: testAnnotations, + }, + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: testLabel, + }, + UpdateStrategy: appsv1.DaemonSetUpdateStrategy{ + Type: appsv1.OnDeleteDaemonSetStrategyType, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: testLabel, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: testContainerName, + Image: testImg1, + Command: []string{"/bin/sh"}, + Args: []string{"-c", "while true; do echo hello; sleep 10;done"}, + }, + }, + }, + }, + }, + } + + Eventually(func() error { + return k8sClient.Create(ctx, testDaemonSet) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + } + + updateDaemonSet := func() { + Eventually(func() error { + testDaemonSet := &appsv1.DaemonSet{} + if err := k8sClient.Get(ctx, client.ObjectKey{ + Name: daemonSetName, + Namespace: namespaceName, + }, testDaemonSet); err != nil { + return err + } + testDaemonSet.Spec.Template.Spec.Containers[0].Image = testImg2 + return k8sClient.Update(ctx, testDaemonSet) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(SatisfyAny(BeNil())) + } + + checkPodStatusAndUpdate := func() { + nodeToImageMap = map[string]string{} + Eventually(func() error { + testPods := &corev1.PodList{} + if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"app": daemonSetName}); err != nil { + return err + } + if len(testPods.Items) != 2 { + return fmt.Errorf("not reconcile") + } + for _, pod := range testPods.Items { + if pod.Status.Phase != corev1.PodRunning { + return fmt.Errorf("not running") + } + nodeToImageMap[pod.Spec.NodeName] = pod.Spec.Containers[0].Image + } + return nil + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(SatisfyAny(BeNil())) + } + + checkNodeStatus := func(nodeName string) error { + node := &corev1.Node{} + if err := k8sClient.Get(ctx, client.ObjectKey{Name: nodeName}, node); err != nil { + return err + } + for _, condition := range node.Status.Conditions { + if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue { + return nil + } + } + return fmt.Errorf("node openyurt-e2e-test-worker2 is not ready") + } + + reconnectNode := func(nodeName string) { + // reconnect node + cmd := exec.Command("/bin/bash", "-c", "docker network connect kind "+nodeName) + err := cmd.Run() + Expect(err).NotTo(HaveOccurred(), "fail to reconnect "+nodeName+" node to kind bridge") + + Eventually(func() error { + return checkNodeStatus(nodeName) + }).WithTimeout(120 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) + + // restart flannel pod on node to recover flannel NIC + Eventually(func() error { + flannelPods := &corev1.PodList{} + if err := k8sClient.List(ctx, flannelPods, client.InNamespace(FlannelNamespace)); err != nil { + return err + } + if len(flannelPods.Items) != 3 { + return fmt.Errorf("not reconcile") + } + for _, pod := range flannelPods.Items { + if pod.Spec.NodeName == nodeName { + if err := k8sClient.Delete(ctx, &pod); err != nil { + return err + } + } + } + return nil + }).WithTimeout(timeout).Should(SatisfyAny(BeNil())) + } + + BeforeEach(func() { + By("Start to run daemonPodUpdater test, clean up previous resources") + nodeToImageMap = map[string]string{} + k8sClient = ycfg.YurtE2eCfg.RuntimeClient + namespaceName = "daemonpodupdater-e2e-test" + "-" + rand.String(4) + createNamespace() + }) + + AfterEach(func() { + By("Cleanup resources after test") + By(fmt.Sprintf("Delete the entire namespaceName %s", namespaceName)) + + Expect(k8sClient.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}}, client.PropagationPolicy(metav1.DeletePropagationBackground))).Should(BeNil()) + }) + + Describe("Test DaemonPodUpdater auto upgrade model", func() { + It("Test one worker disconnect", func() { + By("Run daemonset auto upgrade model test") + updateStrategyType = "Auto" + + createDaemonSet() + checkPodStatusAndUpdate() + + // disconnect openyurt-e2e-test-worker2 node + cmd := exec.Command("/bin/bash", "-c", "docker network disconnect kind openyurt-e2e-test-worker2") + err := cmd.Run() + Expect(err).NotTo(HaveOccurred(), "fail to disconnect openyurt-e2e-test-worker2 node to kind bridge: docker network disconnect kind %s") + Eventually(func() error { + return checkNodeStatus("openyurt-e2e-test-worker2") + }).WithTimeout(120 * time.Second).WithPolling(1 * time.Second).Should(SatisfyAll(HaveOccurred(), Not(&util.NotFoundMatcher{}))) + + // update the daemonset + updateDaemonSet() + + // check image version + Eventually(func() error { + checkPodStatusAndUpdate() + if nodeToImageMap["openyurt-e2e-test-worker"] == testImg2 && nodeToImageMap["openyurt-e2e-test-worker2"] == testImg1 { + return nil + } + return fmt.Errorf("error image update") + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(Succeed()) + + // recover network environment + reconnectNode("openyurt-e2e-test-worker2") + + // check image version + Eventually(func() error { + checkPodStatusAndUpdate() + if nodeToImageMap["openyurt-e2e-test-worker"] == testImg2 && nodeToImageMap["openyurt-e2e-test-worker2"] == testImg2 { + return nil + } + return fmt.Errorf("error image update") + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(Succeed()) + }) + + AfterEach(func() { + By("Reconnect openyurt-e2e-test-worker2 node if it is disconnected") + if err := checkNodeStatus("openyurt-e2e-test-worker2"); err == nil { + return + } + // reconnect openyurt-e2e-test-worker2 node to avoid impact on other tests + reconnectNode("openyurt-e2e-test-worker2") + }) + }) + + Describe("Test DaemonPodUpdater ota upgrade model", func() { + It("Test ota update for one worker", func() { + By("Run daemonset ota upgrade model test") + var pN2 string + updateStrategyType = "OTA" + + createDaemonSet() + checkPodStatusAndUpdate() + + // update the daemonset + updateDaemonSet() + + // check status condition PodNeedUpgrade + Eventually(func() error { + testPods := &corev1.PodList{} + if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"app": daemonSetName}); err != nil { + return err + } + if len(testPods.Items) != 2 { + return fmt.Errorf("not reconcile") + } + for _, pod := range testPods.Items { + for _, condition := range pod.Status.Conditions { + if condition.Type == PodNeedUpgrade && condition.Status != corev1.ConditionTrue { + return fmt.Errorf("pod %s status condition PodNeedUpgrade is not true", pod.Name) + } + } + if pod.Spec.NodeName == "openyurt-e2e-test-worker2" { + pN2 = pod.Name + } + } + return nil + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(SatisfyAny(BeNil())) + + // ota update for openyurt-e2e-test-worker2 node + Eventually(func() string { + curlCmd := fmt.Sprintf("curl -X POST %s:%s/openyurt.io/v1/namespaces/%s/pods/%s/upgrade", ServerName, ServerPort, namespaceName, pN2) + opBytes, err := exec.Command("/bin/bash", "-c", "docker exec -t openyurt-e2e-test-worker2 /bin/bash -c '"+curlCmd+"'").CombinedOutput() + + if err != nil { + return "" + } + return string(opBytes) + }).WithTimeout(10*time.Second).WithPolling(1*time.Second).Should(ContainSubstring("Start updating pod"), "fail to ota update for pod") + + // check image version + Eventually(func() error { + checkPodStatusAndUpdate() + if nodeToImageMap["openyurt-e2e-test-worker"] == testImg1 && nodeToImageMap["openyurt-e2e-test-worker2"] == testImg2 { + return nil + } + return fmt.Errorf("error image update") + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(Succeed()) + }) + }) +}) From 1e0eb475e461bfffda8191229766959c5e263622 Mon Sep 17 00:00:00 2001 From: TonyZZhang <39551789+TonyZZhang@users.noreply.github.com> Date: Mon, 5 Jun 2023 14:05:09 +0800 Subject: [PATCH 10/93] fix typo (#1513) --- pkg/yurthub/cachemanager/cache_manager.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/yurthub/cachemanager/cache_manager.go b/pkg/yurthub/cachemanager/cache_manager.go index ed677a196fb..1d1769582e8 100644 --- a/pkg/yurthub/cachemanager/cache_manager.go +++ b/pkg/yurthub/cachemanager/cache_manager.go @@ -228,7 +228,7 @@ func (cm *cacheManager) queryOneObject(req *http.Request) (runtime.Object, error comp, _ := util.ClientComponentFrom(ctx) // query in-memory cache first - var isInMemoryCache = isInMemeoryCache(ctx) + var isInMemoryCache = isInMemoryCache(ctx) var isInMemoryCacheMiss bool if isInMemoryCache { if obj, err := cm.queryInMemeryCache(info); err != nil { @@ -602,7 +602,7 @@ func (cm *cacheManager) saveOneObject(ctx context.Context, info *apirequest.Requ } // update the in-memory cache with cloud response - if !isInMemeoryCache(ctx) { + if !isInMemoryCache(ctx) { return nil } // When reaching here, it means the obj in backend storage has been updated/created successfully, @@ -824,9 +824,9 @@ func isKubeletPodRequest(req *http.Request) bool { return false } -// isInMemmoryCache verify if the response of the request should be cached in-memory. +// isInMemoryCache verify if the response of the request should be cached in-memory. // In order to accelerate kubelet get node and lease object, we cache them -func isInMemeoryCache(reqCtx context.Context) bool { +func isInMemoryCache(reqCtx context.Context) bool { var comp, resource string var reqInfo *apirequest.RequestInfo var ok bool From 61f52998818d310b23bd39f3ac9b9261ba852c0d Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Mon, 5 Jun 2023 17:02:09 +0800 Subject: [PATCH 11/93] improve yurtstaticset template metadata (#1486) --- charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml | 2 -- charts/yurthub/templates/yurthub-yurtstaticset.yaml | 2 -- pkg/apis/apps/v1alpha1/default.go | 4 ++++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml b/charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml index 40a3abdddcc..a287fac1aa0 100644 --- a/charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml +++ b/charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml @@ -11,8 +11,6 @@ spec: metadata: labels: k8s-app: yurt-hub-cloud - name: yurt-hub-cloud - namespace: {{ .Release.Namespace }} spec: volumes: - name: hub-dir diff --git a/charts/yurthub/templates/yurthub-yurtstaticset.yaml b/charts/yurthub/templates/yurthub-yurtstaticset.yaml index e62c0083fbc..172dc91576e 100644 --- a/charts/yurthub/templates/yurthub-yurtstaticset.yaml +++ b/charts/yurthub/templates/yurthub-yurtstaticset.yaml @@ -11,8 +11,6 @@ spec: metadata: labels: k8s-app: yurt-hub - name: yurt-hub - namespace: {{ .Release.Namespace }} spec: volumes: - name: hub-dir diff --git a/pkg/apis/apps/v1alpha1/default.go b/pkg/apis/apps/v1alpha1/default.go index bf7c18d917e..8069f55e412 100644 --- a/pkg/apis/apps/v1alpha1/default.go +++ b/pkg/apis/apps/v1alpha1/default.go @@ -227,6 +227,10 @@ func SetDefaultsYurtStaticSet(obj *YurtStaticSet) { if podSpec != nil { SetDefaultPodSpec(podSpec) } + + // use YurtStaticSet name and namespace to replace name and namespace in template metadata + obj.Spec.Template.Name = obj.Name + obj.Spec.Template.Namespace = obj.Namespace } // SetDefaultsYurtAppDaemon set default values for YurtAppDaemon. From 68893e4336e72b8ce768bd3af08383e86eaaea69 Mon Sep 17 00:00:00 2001 From: Lancelot <1984737645@qq.com> Date: Tue, 6 Jun 2023 11:43:09 +0800 Subject: [PATCH 12/93] feat: use real kubernetes server address to yurthub when yurtadm join (#1517) Signed-off-by: Lancelot <1984737645@qq.com> --- pkg/yurtadm/constants/constants.go | 1 + pkg/yurtadm/util/yurthub/yurthub.go | 30 +++ pkg/yurtadm/util/yurthub/yurthub_test.go | 227 +++++++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 pkg/yurtadm/util/yurthub/yurthub_test.go diff --git a/pkg/yurtadm/constants/constants.go b/pkg/yurtadm/constants/constants.go index f18a1d226de..780eeae57d4 100644 --- a/pkg/yurtadm/constants/constants.go +++ b/pkg/yurtadm/constants/constants.go @@ -107,6 +107,7 @@ const ( // ReuseCNIBin flag sets whether to reuse local CNI binaries or not. ReuseCNIBin = "reuse-cni-bin" + DefaultServerAddr = "https://127.0.0.1:6443" ServerHealthzServer = "127.0.0.1:10267" ServerHealthzURLPath = "/v1/healthz" ServerReadyzURLPath = "/v1/readyz" diff --git a/pkg/yurtadm/util/yurthub/yurthub.go b/pkg/yurtadm/util/yurthub/yurthub.go index 186b23f4b24..4671bb6a179 100644 --- a/pkg/yurtadm/util/yurthub/yurthub.go +++ b/pkg/yurtadm/util/yurthub/yurthub.go @@ -17,6 +17,8 @@ limitations under the License. package yurthub import ( + "bufio" + "bytes" "fmt" "io" "net/http" @@ -73,6 +75,12 @@ func AddYurthubStaticYaml(data joindata.YurtJoinData, podManifestPath string) er if err != nil { return err } + + yurthubTemplate, err = useRealServerAddr(yurthubTemplate, kubernetesServerAddrs) + if err != nil { + return err + } + yurthubManifestFile := filepath.Join(podManifestPath, util.WithYamlSuffix(data.YurtHubManifest())) klog.Infof("yurthub template: %s\n%s", yurthubManifestFile, yurthubTemplate) @@ -170,3 +178,25 @@ func CleanHubBootstrapConfig() error { } return nil } + +// useRealServerAddr check if the server-addr from yurthubTemplate is default value: 127.0.0.1:6443 +// if yes, we should use the real server addr +func useRealServerAddr(yurthubTemplate string, kubernetesServerAddrs string) (string, error) { + scanner := bufio.NewScanner(bytes.NewReader([]byte(yurthubTemplate))) + var buffer bytes.Buffer + target := fmt.Sprintf("%v=%v", constants.ServerAddr, constants.DefaultServerAddr) + + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, target) { + line = strings.Replace(line, constants.DefaultServerAddr, kubernetesServerAddrs, -1) + } + buffer.WriteString(line + "\n") + } + + if err := scanner.Err(); err != nil { + klog.Infof("Error scanning file: %v\n", err) + return "", err + } + return buffer.String(), nil +} diff --git a/pkg/yurtadm/util/yurthub/yurthub_test.go b/pkg/yurtadm/util/yurthub/yurthub_test.go new file mode 100644 index 00000000000..78c005e805d --- /dev/null +++ b/pkg/yurtadm/util/yurthub/yurthub_test.go @@ -0,0 +1,227 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package yurthub + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + defaultAddr = `apiVersion: v1 +kind: Pod +metadata: + annotations: + openyurt.io/static-pod-hash: 76f4f955b6 + creationTimestamp: null + labels: + k8s-app: yurt-hub + name: yurt-hub + namespace: kube-system +spec: + containers: + - command: + - yurthub + - --v=2 + - --bind-address=127.0.0.1 + - --server-addr=https://127.0.0.1:6443 + - --node-name=$(NODE_NAME) + - --bootstrap-file=/var/lib/yurthub/bootstrap-hub.conf + - --working-mode=edge + - --namespace=kube-system + env: + - name: NODE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + image: openyurt/yurthub:v1.3.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + host: 127.0.0.1 + path: /v1/healthz + port: 10267 + scheme: HTTP + initialDelaySeconds: 300 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: yurt-hub + resources: + limits: + memory: 300Mi + requests: + cpu: 150m + memory: 150Mi + securityContext: + capabilities: + add: + - NET_ADMIN + - NET_RAW + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/lib/yurthub + name: hub-dir + - mountPath: /etc/kubernetes + name: kubernetes + dnsPolicy: ClusterFirst + hostNetwork: true + priority: 2000001000 + priorityClassName: system-node-critical + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - hostPath: + path: /var/lib/yurthub + type: DirectoryOrCreate + name: hub-dir + - hostPath: + path: /etc/kubernetes + type: Directory + name: kubernetes +status: {} +` + + setAddr = `apiVersion: v1 +kind: Pod +metadata: + annotations: + openyurt.io/static-pod-hash: 76f4f955b6 + creationTimestamp: null + labels: + k8s-app: yurt-hub + name: yurt-hub + namespace: kube-system +spec: + containers: + - command: + - yurthub + - --v=2 + - --bind-address=127.0.0.1 + - --server-addr=https://192.0.0.1:6443 + - --node-name=$(NODE_NAME) + - --bootstrap-file=/var/lib/yurthub/bootstrap-hub.conf + - --working-mode=edge + - --namespace=kube-system + env: + - name: NODE_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: spec.nodeName + image: openyurt/yurthub:v1.3.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + host: 127.0.0.1 + path: /v1/healthz + port: 10267 + scheme: HTTP + initialDelaySeconds: 300 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + name: yurt-hub + resources: + limits: + memory: 300Mi + requests: + cpu: 150m + memory: 150Mi + securityContext: + capabilities: + add: + - NET_ADMIN + - NET_RAW + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/lib/yurthub + name: hub-dir + - mountPath: /etc/kubernetes + name: kubernetes + dnsPolicy: ClusterFirst + hostNetwork: true + priority: 2000001000 + priorityClassName: system-node-critical + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - hostPath: + path: /var/lib/yurthub + type: DirectoryOrCreate + name: hub-dir + - hostPath: + path: /etc/kubernetes + type: Directory + name: kubernetes +status: {} +` + + serverAddrsA = "https://192.0.0.1:6443" + serverAddrsB = "https://192.0.0.2:6443" +) + +func Test_useRealServerAddr(t *testing.T) { + type args struct { + yurthubTemplate string + kubernetesServerAddrs string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "change default server addr", + args: args{ + yurthubTemplate: defaultAddr, + kubernetesServerAddrs: serverAddrsA, + }, + want: setAddr, + }, + { + name: " already set server addr", + args: args{ + yurthubTemplate: setAddr, + kubernetesServerAddrs: serverAddrsB, + }, + want: setAddr, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + actualYaml, err := useRealServerAddr(test.args.yurthubTemplate, test.args.kubernetesServerAddrs) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + assert.Equal(t, actualYaml, test.want) + }) + } +} From eed5149ddb488ca3a631bf756564825a1262281d Mon Sep 17 00:00:00 2001 From: vie-serendipity <60083692+vie-serendipity@users.noreply.github.com> Date: Tue, 6 Jun 2023 14:40:09 +0800 Subject: [PATCH 13/93] fix typo (#1519) --- pkg/controller/yurtstaticset/yurtstaticset_controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/controller/yurtstaticset/yurtstaticset_controller.go b/pkg/controller/yurtstaticset/yurtstaticset_controller.go index e34e93c3f81..6016c796ce3 100644 --- a/pkg/controller/yurtstaticset/yurtstaticset_controller.go +++ b/pkg/controller/yurtstaticset/yurtstaticset_controller.go @@ -216,7 +216,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { } // 4. Watch for changes of static pods - reconcileYurtStatisSetForStaticPod := func(obj client.Object) []reconcile.Request { + reconcileYurtStaticSetForStaticPod := func(obj client.Object) []reconcile.Request { var reqs []reconcile.Request pod, ok := obj.(*corev1.Pod) if !ok { @@ -237,7 +237,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { } if err := c.Watch(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc( func(obj client.Object) []reconcile.Request { - return reconcileYurtStatisSetForStaticPod(obj) + return reconcileYurtStaticSetForStaticPod(obj) })); err != nil { return err } From 6ad4da6f656d9c8506e7ebde139535bdd7d3c4e9 Mon Sep 17 00:00:00 2001 From: Lancelot <1984737645@qq.com> Date: Tue, 6 Jun 2023 15:40:09 +0800 Subject: [PATCH 14/93] feat: cleanup "yurt-manager" temporary dir after "local-up-openyurt" (#1522) Signed-off-by: Lancelot <1984737645@qq.com> --- test/e2e/cmd/init/converter.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/e2e/cmd/init/converter.go b/test/e2e/cmd/init/converter.go index 39f9d58a8a6..5f61b907afb 100644 --- a/test/e2e/cmd/init/converter.go +++ b/test/e2e/cmd/init/converter.go @@ -19,7 +19,6 @@ package init import ( "context" "fmt" - "io/ioutil" "os" "path/filepath" "strconv" @@ -254,7 +253,10 @@ func (c *ClusterConverter) deployYurtManager() error { if err != nil { return err } - defer os.Remove(renderedFile) + + // get renderedFile parent dir + renderedFileDir := filepath.Dir(renderedFile) + defer os.RemoveAll(renderedFileDir) if err := c.ComponentsBuilder.InstallComponents(renderedFile, false); err != nil { return err } @@ -296,13 +298,13 @@ func (c *ClusterConverter) deployYurtManager() error { // generatedAutoGeneratedTempFile will replace {{ .Release.Namespace }} with ns in webhooks func generatedAutoGeneratedTempFile(root, ns string) (string, error) { - tempDir, err := ioutil.TempDir(root, "yurt-manager") + tempDir, err := os.MkdirTemp(root, "yurt-manager") if err != nil { return "", err } autoGeneratedYaml := filepath.Join(root, "charts/yurt-manager/templates/yurt-manager-auto-generated.yaml") - contents, err := ioutil.ReadFile(autoGeneratedYaml) + contents, err := os.ReadFile(autoGeneratedYaml) if err != nil { return "", err } @@ -317,5 +319,5 @@ func generatedAutoGeneratedTempFile(root, ns string) (string, error) { tempFile := filepath.Join(tempDir, "yurt-manager-auto-generated.yaml") klog.Infof("rendered yurt-manager-auto-generated.yaml file: \n%s\n", newContents) - return tempFile, ioutil.WriteFile(tempFile, []byte(newContents), 0644) + return tempFile, os.WriteFile(tempFile, []byte(newContents), 0644) } From f923eef7f2c85882a89f3cf262d0823a15d7e02f Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Tue, 6 Jun 2023 15:40:26 +0800 Subject: [PATCH 15/93] improve yurtstaticset upgrade for existing static pod (#1516) --- .../yurtstaticset/upgradeinfo/upgrade_info.go | 82 ++++++++- .../upgradeinfo/upgrade_info_test.go | 158 ++++++++++++++++++ 2 files changed, 237 insertions(+), 3 deletions(-) diff --git a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go b/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go index 16fa5444d20..ee52d274061 100644 --- a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go +++ b/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go @@ -17,11 +17,13 @@ limitations under the License. package upgradeinfo import ( + "bytes" "context" "fmt" "strings" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/json" "k8s.io/kubectl/pkg/util/podutils" "sigs.k8s.io/controller-runtime/pkg/client" @@ -81,7 +83,7 @@ func New(c client.Client, instance *appsv1alpha1.YurtStaticSet, workerPodName, h // The name format of mirror static pod is `StaticPodName-NodeName` if util.Hyphen(instance.Name, nodeName) == pod.Name && util.IsStaticPod(&pod) { // initialize static pod info - if err := initStaticPodInfo(c, nodeName, hash, &podList.Items[i], infos); err != nil { + if err := initStaticPodInfo(instance, c, nodeName, hash, &podList.Items[i], infos); err != nil { return nil, err } } @@ -98,15 +100,21 @@ func New(c client.Client, instance *appsv1alpha1.YurtStaticSet, workerPodName, h return infos, nil } -func initStaticPodInfo(c client.Client, nodeName, hash string, pod *corev1.Pod, infos map[string]*UpgradeInfo) error { +func initStaticPodInfo(instance *appsv1alpha1.YurtStaticSet, c client.Client, nodeName, hash string, + pod *corev1.Pod, infos map[string]*UpgradeInfo) error { + if info := infos[nodeName]; info == nil { infos[nodeName] = &UpgradeInfo{} } infos[nodeName].StaticPod = pod - if pod.Annotations[StaticPodHashAnnotation] != hash { + hashAnnotation, ok := pod.Annotations[StaticPodHashAnnotation] + if ok && hashAnnotation != hash { // Indicate the static pod in this node needs to be upgraded infos[nodeName].UpgradeNeeded = true + } else if !ok && !match(instance, pod) { + // Indicate the static pod which is already existing and has no hash + infos[nodeName].UpgradeNeeded = true } // Sets the ready status static pod @@ -149,6 +157,74 @@ func initWorkerPodInfo(nodeName, hash string, pod *corev1.Pod, infos map[string] return nil } +// match check if the given YurtStaticSet's template matches the pod. +func match(instance *appsv1alpha1.YurtStaticSet, pod *corev1.Pod) bool { + + yssBytes, err := json.Marshal(instance.Spec.Template) + if err != nil { + return false + } + var yssRaw map[string]interface{} + err = json.Unmarshal(yssBytes, &yssRaw) + if err != nil { + return false + } + yssSpec := yssRaw["spec"].(map[string]interface{}) + yssMetadata := yssRaw["metadata"].(map[string]interface{}) + delete(yssMetadata, "name") + delete(yssMetadata, "creationTimestamp") + + podBytes, err := json.Marshal(pod) + if err != nil { + return false + } + var podRaw map[string]interface{} + err = json.Unmarshal(podBytes, &podRaw) + if err != nil { + return false + } + podSpec := podRaw["spec"].(map[string]interface{}) + podMetadata := podRaw["metadata"].(map[string]interface{}) + + for k, v := range yssSpec { + if value, ok := podSpec[k]; ok { + byte1, err := json.Marshal(value) + if err != nil { + return false + } + byte2, err := json.Marshal(v) + if err != nil { + return false + } + if !bytes.Equal(byte1, byte2) { + return false + } + } else { + return false + } + } + + for k, v := range yssMetadata { + if value, ok := podMetadata[k]; ok { + byte1, err := json.Marshal(value) + if err != nil { + return false + } + byte2, err := json.Marshal(v) + if err != nil { + return false + } + if !bytes.Equal(byte1, byte2) { + return false + } + } else { + return false + } + } + + return true +} + // ReadyUpgradeWaitingNodes gets those nodes that satisfied // 1. node is ready // 2. node needs to be upgraded diff --git a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go b/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go index 668e515d55b..234a3b6b1c7 100644 --- a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go +++ b/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go @@ -23,6 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" + utilpointer "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -237,3 +238,160 @@ func hasCommonElementForPod(a []string, b []*corev1.Pod) bool { } return true } + +var ( + hostPathDirectoryOrCreate = corev1.HostPathDirectoryOrCreate + testYss = &appsv1alpha1.YurtStaticSet{ + Spec: appsv1alpha1.YurtStaticSetSpec{ + StaticPodManifest: "yurthub", + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "k8s-app": "yurthub", + }, + Name: "yurthub", + Namespace: "kube-system", + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "hub-dir", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: "/var/lib/yurthub", + Type: &hostPathDirectoryOrCreate, + }, + }, + }, + }, + HostNetwork: true, + PriorityClassName: "system-node-critical", + Priority: utilpointer.Int32Ptr(2000001000), + }, + }, + }, + } +) + +func preparePods() []*corev1.Pod { + podList := make([]*corev1.Pod, 0) + testPod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "k8s-app": "yurthub", + }, + Name: "yurt-hub-host-475424", + Namespace: "kube-system", + ResourceVersion: "111112", + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "hub-dir", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: "/var/lib/yurthub", + Type: &hostPathDirectoryOrCreate, + }, + }, + }, + }, + HostNetwork: true, + PriorityClassName: "system-node-critical", + Priority: utilpointer.Int32Ptr(2000001000), + NodeName: "aaa", + SchedulerName: "default-scheduler", + RestartPolicy: "Always", + }, + } + podList = append(podList, testPod) + + testPod2 := testPod.DeepCopy() + testPod2.Spec.PriorityClassName = "aaaa" + podList = append(podList, testPod2) + + testPod3 := testPod.DeepCopy() + testPod3.Spec = corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "hub-dir", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: "/var/lib/yurthub", + Type: &hostPathDirectoryOrCreate, + }, + }, + }, + }, + PriorityClassName: "system-node-critical", + Priority: utilpointer.Int32Ptr(2000001000), + NodeName: "aaa", + SchedulerName: "default-scheduler", + RestartPolicy: "Always", + } + podList = append(podList, testPod3) + + testPod4 := testPod.DeepCopy() + testPod4.Namespace = "fffff" + podList = append(podList, testPod4) + + testPod5 := testPod.DeepCopy() + testPod5.ObjectMeta = metav1.ObjectMeta{ + Labels: map[string]string{ + "k8s-app": "yurthub", + }, + Name: "yurt-hub-host-475424", + ResourceVersion: "111112", + } + podList = append(podList, testPod5) + + return podList +} + +func TestMatch(t *testing.T) { + pods := preparePods() + tests := []struct { + name string + instance *appsv1alpha1.YurtStaticSet + pod *corev1.Pod + want bool + }{ + { + name: "test1", + instance: testYss, + pod: pods[0], + want: true, + }, + { + name: "test2", + instance: testYss, + pod: pods[1], + want: false, + }, + { + name: "test3", + instance: testYss, + pod: pods[2], + want: false, + }, + { + name: "test4", + instance: testYss, + pod: pods[3], + want: false, + }, + { + name: "test5", + instance: testYss, + pod: pods[4], + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := match(tt.instance, tt.pod); got != tt.want { + t.Errorf("match() = %v, want %v", got, tt.want) + } + }) + } +} From c0a1a4cc8424e694b4b7663b67f6cef0916b64d2 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Tue, 6 Jun 2023 19:33:09 +0800 Subject: [PATCH 16/93] fix yurthub memory leak (#1501) --- pkg/yurthub/poolcoordinator/coordinator.go | 4 +++- pkg/yurthub/storage/etcd/keycache.go | 7 +++++-- pkg/yurthub/storage/etcd/storage.go | 6 ++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/yurthub/poolcoordinator/coordinator.go b/pkg/yurthub/poolcoordinator/coordinator.go index cf85e8be299..194f9933260 100644 --- a/pkg/yurthub/poolcoordinator/coordinator.go +++ b/pkg/yurthub/poolcoordinator/coordinator.go @@ -288,9 +288,9 @@ func (coordinator *coordinator) Run() { continue } - klog.Infof("coordinator newCloudLeaseClient success.") if err := coordinator.poolCacheSyncManager.EnsureStart(); err != nil { klog.Errorf("failed to sync pool-scoped resource, %v", err) + cancelEtcdStorage() coordinator.statusInfoChan <- electorStatusInfo continue } @@ -299,9 +299,11 @@ func (coordinator *coordinator) Run() { nodeLeaseProxyClient, err := coordinator.newNodeLeaseProxyClient() if err != nil { klog.Errorf("cloud not get cloud lease client when becoming leader yurthub, %v", err) + cancelEtcdStorage() coordinator.statusInfoChan <- electorStatusInfo continue } + klog.Infof("coordinator newCloudLeaseClient success.") coordinator.delegateNodeLeaseManager.EnsureStartWithHandler(cache.FilteringResourceEventHandler{ FilterFunc: ifDelegateHeartBeat, Handler: cache.ResourceEventHandlerFuncs{ diff --git a/pkg/yurthub/storage/etcd/keycache.go b/pkg/yurthub/storage/etcd/keycache.go index 43357ef7170..28a76515fe3 100644 --- a/pkg/yurthub/storage/etcd/keycache.go +++ b/pkg/yurthub/storage/etcd/keycache.go @@ -108,9 +108,12 @@ func (c *componentKeyCache) Recover() error { func (c *componentKeyCache) getPoolScopedKeyset() (*keyCache, error) { keys := &keyCache{m: make(map[schema.GroupVersionResource]storageKeySet)} - for _, gvr := range c.poolScopedResourcesGetter() { + getFunc := func(key string) (*clientv3.GetResponse, error) { getCtx, cancel := context.WithTimeout(c.ctx, defaultTimeout) defer cancel() + return c.etcdClient.Get(getCtx, key, clientv3.WithPrefix(), clientv3.WithKeysOnly()) + } + for _, gvr := range c.poolScopedResourcesGetter() { rootKey, err := c.keyFunc(storage.KeyBuildInfo{ Component: coordinatorconstants.DefaultPoolScopedUserAgent, Group: gvr.Group, @@ -120,7 +123,7 @@ func (c *componentKeyCache) getPoolScopedKeyset() (*keyCache, error) { if err != nil { return nil, fmt.Errorf("failed to generate keys for %s, %v", gvr.String(), err) } - getResp, err := c.etcdClient.Get(getCtx, rootKey.Key(), clientv3.WithPrefix(), clientv3.WithKeysOnly()) + getResp, err := getFunc(rootKey.Key()) if err != nil { return nil, fmt.Errorf("failed to get from etcd for %s, %v", gvr.String(), err) } diff --git a/pkg/yurthub/storage/etcd/storage.go b/pkg/yurthub/storage/etcd/storage.go index 2897ed31fdb..61fc10a4fb7 100644 --- a/pkg/yurthub/storage/etcd/storage.go +++ b/pkg/yurthub/storage/etcd/storage.go @@ -141,6 +141,9 @@ func NewStorage(ctx context.Context, cfg *EtcdStorageConfig) (storage.Store, err poolScopedResourcesGetter: resources.GetPoolScopeResources, } if err := cache.Recover(); err != nil { + if err := client.Close(); err != nil { + return nil, fmt.Errorf("failed to close etcd client, %v", err) + } return nil, fmt.Errorf("failed to recover component key cache from %s, %v", cacheFilePath, err) } s.localComponentKeyCache = cache @@ -182,6 +185,9 @@ func (s *etcdStorage) clientLifeCycleManagement() { for { select { case <-s.ctx.Done(): + if err := s.client.Close(); err != nil { + klog.Errorf("failed to close etcd client, %v", err) + } klog.Info("etcdstorage lifecycle routine exited") return default: From 2c6dd31a5fac1bff078bd15f2a9086088d99da65 Mon Sep 17 00:00:00 2001 From: Liang Deng <283304489@qq.com> Date: Wed, 7 Jun 2023 14:07:11 +0800 Subject: [PATCH 17/93] fix: yurtadm support enable kubelet service (#1523) Signed-off-by: Liang Deng <283304489@qq.com> --- pkg/yurtadm/cmd/join/phases/prepare.go | 3 ++ pkg/yurtadm/util/initsystem/initsystem.go | 6 +++ .../util/initsystem/initsystem_unix.go | 40 +++++++++++++++++ .../util/initsystem/initsystem_windows.go | 44 +++++++++++++++++++ pkg/yurtadm/util/kubernetes/kubernetes.go | 15 +++++++ 5 files changed, 108 insertions(+) diff --git a/pkg/yurtadm/cmd/join/phases/prepare.go b/pkg/yurtadm/cmd/join/phases/prepare.go index 8b3879da626..a4839a4a00a 100644 --- a/pkg/yurtadm/cmd/join/phases/prepare.go +++ b/pkg/yurtadm/cmd/join/phases/prepare.go @@ -58,6 +58,9 @@ func RunPrepare(data joindata.YurtJoinData) error { if err := yurtadmutil.SetKubeletService(); err != nil { return err } + if err := yurtadmutil.EnableKubeletService(); err != nil { + return err + } if err := yurtadmutil.SetKubeletUnitConfig(); err != nil { return err } diff --git a/pkg/yurtadm/util/initsystem/initsystem.go b/pkg/yurtadm/util/initsystem/initsystem.go index b59c399418c..96c07ea5b15 100644 --- a/pkg/yurtadm/util/initsystem/initsystem.go +++ b/pkg/yurtadm/util/initsystem/initsystem.go @@ -19,6 +19,12 @@ package initsystem // InitSystem is the interface that describe behaviors of an init system type InitSystem interface { + // ServiceIsEnabled ensures the service is enabled to start on each boot. + ServiceIsEnabled(service string) bool + + // ServiceEnable tries to enable a specific service + ServiceEnable(service string) error + // ServiceIsActive ensures the service is running, or attempting to run. (crash looping in the case of kubelet) ServiceIsActive(service string) bool } diff --git a/pkg/yurtadm/util/initsystem/initsystem_unix.go b/pkg/yurtadm/util/initsystem/initsystem_unix.go index 40f06510be4..4acf4dca454 100644 --- a/pkg/yurtadm/util/initsystem/initsystem_unix.go +++ b/pkg/yurtadm/util/initsystem/initsystem_unix.go @@ -24,11 +24,26 @@ import ( "fmt" "os/exec" "strings" + + "github.com/pkg/errors" ) // OpenRCInitSystem defines openrc type OpenRCInitSystem struct{} +// ServiceIsEnabled ensures the service is enabled to start on each boot. +func (openrc OpenRCInitSystem) ServiceIsEnabled(service string) bool { + args := []string{"show", "default"} + outBytes, _ := exec.Command("rc-update", args...).Output() + return strings.Contains(string(outBytes), service) +} + +// ServiceEnable tries to start a specific service +func (openrc OpenRCInitSystem) ServiceEnable(service string) error { + args := []string{"add", service, "default"} + return exec.Command("rc-update", args...).Run() +} + // ServiceIsActive ensures the service is running, or attempting to run. (crash looping in the case of kubelet) func (openrc OpenRCInitSystem) ServiceIsActive(service string) bool { args := []string{service, "status"} @@ -40,6 +55,31 @@ func (openrc OpenRCInitSystem) ServiceIsActive(service string) bool { // SystemdInitSystem defines systemd type SystemdInitSystem struct{} +// reloadSystemd reloads the systemd daemon +func (sysd SystemdInitSystem) reloadSystemd() error { + if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil { + return errors.Wrap(err, "failed to reload systemd") + } + return nil +} + +// ServiceIsEnabled ensures the service is enabled to start on each boot. +func (sysd SystemdInitSystem) ServiceIsEnabled(service string) bool { + args := []string{"is-enabled", service} + err := exec.Command("systemctl", args...).Run() + return err == nil +} + +// ServiceEnable tries to start a specific service +func (sysd SystemdInitSystem) ServiceEnable(service string) error { + // Before we try to start any service, make sure that systemd is ready + if err := sysd.reloadSystemd(); err != nil { + return err + } + args := []string{"enable", service} + return exec.Command("systemctl", args...).Run() +} + // ServiceIsActive will check is the service is "active". In the case of // crash looping services (kubelet in our case) status will return as // "activating", so we will consider this active as well. diff --git a/pkg/yurtadm/util/initsystem/initsystem_windows.go b/pkg/yurtadm/util/initsystem/initsystem_windows.go index cea90251b31..6101a2dda51 100644 --- a/pkg/yurtadm/util/initsystem/initsystem_windows.go +++ b/pkg/yurtadm/util/initsystem/initsystem_windows.go @@ -30,6 +30,50 @@ import ( // WindowsInitSystem is the windows implementation of InitSystem type WindowsInitSystem struct{} +// ServiceIsEnabled ensures the service is enabled to start on each boot. +func (sysd WindowsInitSystem) ServiceIsEnabled(service string) bool { + m, err := mgr.Connect() + if err != nil { + return false + } + defer m.Disconnect() + + s, err := m.OpenService(service) + if err != nil { + return false + } + defer s.Close() + + c, err := s.Config() + if err != nil { + return false + } + + return c.StartType != mgr.StartDisabled +} + +func (sysd WindowsInitSystem) ServiceEnable(service string) error { + m, err := mgr.Connect() + if err != nil { + return false + } + defer m.Disconnect() + + s, err := m.OpenService(service) + if err != nil { + return false + } + defer s.Close() + + c, err := s.Config() + if err != nil { + return false + } + c.StartType = mgr.StartAutomatic + + return s.UpdateConfig(c) +} + // ServiceIsActive ensures the service is running, or attempting to run. (crash looping in the case of kubelet) func (sysd WindowsInitSystem) ServiceIsActive(service string) bool { m, err := mgr.Connect() diff --git a/pkg/yurtadm/util/kubernetes/kubernetes.go b/pkg/yurtadm/util/kubernetes/kubernetes.go index 37f22e3732a..31d5de8bf82 100644 --- a/pkg/yurtadm/util/kubernetes/kubernetes.go +++ b/pkg/yurtadm/util/kubernetes/kubernetes.go @@ -270,6 +270,21 @@ func SetKubeletService() error { return nil } +// EnableKubeletService enable kubelet service +func EnableKubeletService() error { + initSystem, err := initsystem.GetInitSystem() + if err != nil { + return err + } + + if !initSystem.ServiceIsEnabled("kubelet") { + if err = initSystem.ServiceEnable("kubelet"); err != nil { + return fmt.Errorf("enable kubelet service failed") + } + } + return nil +} + // SetKubeletUnitConfig configure kubelet startup parameters. func SetKubeletUnitConfig() error { kubeletUnitDir := filepath.Dir(constants.KubeletServiceConfPath) From 40c097d3f9f7cec1ca01d1a0f260e2fbe716b63b Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:19:09 +0800 Subject: [PATCH 18/93] fix yurtstaticset workerpod reset error (#1526) --- pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go | 5 +++-- .../yurtstaticset/upgradeinfo/upgrade_info_test.go | 2 +- pkg/controller/yurtstaticset/yurtstaticset_controller.go | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go b/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go index ee52d274061..be4d1476dbf 100644 --- a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go +++ b/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go @@ -88,8 +88,9 @@ func New(c client.Client, instance *appsv1alpha1.YurtStaticSet, workerPodName, h } } - // The name format of worker pods are `WorkerPodName-NodeName-Hash` Todo: may lead to mismatch - if strings.Contains(pod.Name, workerPodName) { + // The name format of worker pods are `WorkerPodName-YssName-NodeName-Hash` + name := workerPodName + instance.Name + if strings.Contains(pod.Name, name) { // initialize worker pod info if err := initWorkerPodInfo(nodeName, hash, &podList.Items[i], infos); err != nil { return nil, err diff --git a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go b/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go index 234a3b6b1c7..f69c0e4efc0 100644 --- a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go +++ b/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go @@ -60,7 +60,7 @@ func newNodes(nodeNames []string) []client.Object { func newPod(podName string, nodeName string, namespace string, isStaticPod bool) *corev1.Pod { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: UpgradeWorkerPodPrefix + rand.String(10), + Name: UpgradeWorkerPodPrefix + podName + "-" + rand.String(10), Namespace: namespace, }, Spec: corev1.PodSpec{NodeName: nodeName}, diff --git a/pkg/controller/yurtstaticset/yurtstaticset_controller.go b/pkg/controller/yurtstaticset/yurtstaticset_controller.go index 6016c796ce3..0d59909e3f8 100644 --- a/pkg/controller/yurtstaticset/yurtstaticset_controller.go +++ b/pkg/controller/yurtstaticset/yurtstaticset_controller.go @@ -71,7 +71,7 @@ const ( hostPathVolumeSourcePath = hostPathVolumeMountPath // UpgradeWorkerPodPrefix is the name prefix of worker pod which used for static pod upgrade - UpgradeWorkerPodPrefix = "yurt-static-set-upgrade-worker-" + UpgradeWorkerPodPrefix = "yss-upgrade-worker-" UpgradeWorkerContainerName = "upgrade-worker" ArgTmpl = "/usr/local/bin/node-servant static-pod-upgrade --name=%s --namespace=%s --manifest=%s --hash=%s --mode=%s" @@ -499,7 +499,7 @@ func (r *ReconcileYurtStaticSet) removeUnusedPods(pods []*corev1.Pod) error { func createUpgradeWorker(c client.Client, instance *appsv1alpha1.YurtStaticSet, nodes []string, hash, mode, img string) error { for _, node := range nodes { pod := upgradeWorker.DeepCopy() - pod.Name = UpgradeWorkerPodPrefix + util.Hyphen(node, hash) + pod.Name = UpgradeWorkerPodPrefix + instance.Name + "-" + util.Hyphen(node, hash) pod.Namespace = instance.Namespace pod.Spec.NodeName = node metav1.SetMetaDataAnnotation(&pod.ObjectMeta, StaticPodHashAnnotation, hash) From eab81ca065362e758e9e2d0ab4a24a55a186cdb9 Mon Sep 17 00:00:00 2001 From: y-ykcir Date: Wed, 7 Jun 2023 16:21:09 +0800 Subject: [PATCH 19/93] feat: support SIGUSR1 signal for yurthub (#1487) * feat: support SIGUSR1 signal for yurthub Signed-off-by: ricky * set output directory for log Signed-off-by: ricky --------- Signed-off-by: ricky --- cmd/yurthub/app/start.go | 3 ++ pkg/yurthub/util/dumpstack.go | 71 ++++++++++++++++++++++++++++++ pkg/yurthub/util/dumpstack_test.go | 49 +++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 pkg/yurthub/util/dumpstack.go create mode 100644 pkg/yurthub/util/dumpstack_test.go diff --git a/cmd/yurthub/app/start.go b/cmd/yurthub/app/start.go index ca0ad7f35ba..649ef863f0f 100644 --- a/cmd/yurthub/app/start.go +++ b/cmd/yurthub/app/start.go @@ -75,6 +75,9 @@ func NewCmdStartYurtHub(ctx context.Context) *cobra.Command { } klog.Infof("%s cfg: %#+v", projectinfo.GetHubName(), yurtHubCfg) + util.SetupDumpStackTrap(yurtHubOptions.RootDir, ctx.Done()) + klog.Infof("start watch SIGUSR1 signal") + if err := Run(ctx, yurtHubCfg); err != nil { klog.Fatalf("run %s failed, %v", projectinfo.GetHubName(), err) } diff --git a/pkg/yurthub/util/dumpstack.go b/pkg/yurthub/util/dumpstack.go new file mode 100644 index 00000000000..5df324a5a99 --- /dev/null +++ b/pkg/yurthub/util/dumpstack.go @@ -0,0 +1,71 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + "os" + "os/signal" + "path/filepath" + "runtime" + "syscall" + + "k8s.io/klog/v2" +) + +func SetupDumpStackTrap(logDir string, stopCh <-chan struct{}) { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGUSR1) + + go func() { + for { + select { + case <-c: + dumpStacks(true, logDir) + case <-stopCh: + return + } + } + }() +} + +func dumpStacks(writeToFile bool, logDir string) { + var ( + buf []byte + stackSize int + ) + bufferLen := 16384 + for stackSize == len(buf) { + buf = make([]byte, bufferLen) + stackSize = runtime.Stack(buf, true) + bufferLen *= 2 + } + buf = buf[:stackSize] + klog.Infof("=== BEGIN goroutine stack dump ===\n%s\n=== END goroutine stack dump ===", buf) + + if writeToFile { + // Also write to file to aid gathering diagnostics + name := filepath.Join(logDir, fmt.Sprintf("yurthub.%d.stacks.log", os.Getpid())) + f, err := os.Create(name) + if err != nil { + return + } + defer f.Close() + f.WriteString(string(buf)) + klog.Infof("goroutine stack dump written to %s", name) + } +} diff --git a/pkg/yurthub/util/dumpstack_test.go b/pkg/yurthub/util/dumpstack_test.go new file mode 100644 index 00000000000..4b4b8f05bf3 --- /dev/null +++ b/pkg/yurthub/util/dumpstack_test.go @@ -0,0 +1,49 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSetupDumpStackTrap(t *testing.T) { + logDir := "/tmp" + stopCh := make(chan struct{}) + defer close(stopCh) + + SetupDumpStackTrap(logDir, stopCh) + + proc, err := os.FindProcess(os.Getpid()) + assert.NoError(t, err) + assert.NoError(t, proc.Signal(syscall.SIGUSR1)) + + // Wait for a short time to allow stack dump to complete + time.Sleep(time.Millisecond * 100) + + fileName := fmt.Sprintf("yurthub.%d.stacks.log", os.Getpid()) + filePath := filepath.Join(logDir, fileName) + + assert.FileExists(t, filePath) + assert.NoError(t, os.Remove(filePath)) +} From 033f2afddd82a98a0f91383c25b42ba71897f121 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:57:09 +0800 Subject: [PATCH 20/93] build(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.4 (#1507) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.2 to 1.8.4. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.2...v1.8.4) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b15bcb20f6c..68874130aad 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/prometheus/client_golang v1.15.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/vishvananda/netlink v1.2.1-beta.2 go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0 diff --git a/go.sum b/go.sum index 0eca6ab202f..e4d45e0d1d2 100644 --- a/go.sum +++ b/go.sum @@ -651,8 +651,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= From 2c6fe77868116b02fbd2b022d2354ba447a226c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:58:10 +0800 Subject: [PATCH 21/93] build(deps): bump google.golang.org/grpc from 1.54.0 to 1.55.0 (#1432) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.54.0 to 1.55.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.54.0...v1.55.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 68874130aad..5ff4c8b15d2 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 golang.org/x/sys v0.8.0 - google.golang.org/grpc v1.54.0 + google.golang.org/grpc v1.55.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/square/go-jose.v2 v2.6.0 k8s.io/api v0.22.3 @@ -64,7 +64,7 @@ require ( ) require ( - cloud.google.com/go/compute v1.15.1 // indirect + cloud.google.com/go/compute v1.18.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v0.4.1 // indirect @@ -140,14 +140,14 @@ require ( go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.5.0 // indirect golang.org/x/net v0.8.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.2 // indirect diff --git a/go.sum b/go.sum index e4d45e0d1d2..8deca09fc6d 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/compute v1.15.1 h1:7UGq3QknM33pw5xATlpzeoomNxsacIVvTqTTvbfajmE= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -842,8 +842,8 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1044,8 +1044,8 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1061,8 +1061,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 2ddaf2516837a40113136a771cc21d3c9d526e0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:59:09 +0800 Subject: [PATCH 22/93] build(deps): bump zeebe-io/backport-action from 1.2.0 to 1.3.0 (#1515) Bumps [zeebe-io/backport-action](https://github.com/zeebe-io/backport-action) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/zeebe-io/backport-action/releases) - [Commits](https://github.com/zeebe-io/backport-action/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: zeebe-io/backport-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/back-port.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/back-port.yaml b/.github/workflows/back-port.yaml index 821772427c0..40b73bdabdf 100644 --- a/.github/workflows/back-port.yaml +++ b/.github/workflows/back-port.yaml @@ -30,7 +30,7 @@ jobs: with: fetch-depth: 0 - name: Create Backport PR - uses: zeebe-io/backport-action@v1.2.0 + uses: zeebe-io/backport-action@v1.3.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} github_workspace: ${{ github.workspace }} \ No newline at end of file From 585bbd48f7d3846fe451d3f94613d15ccb1da57c Mon Sep 17 00:00:00 2001 From: my0sotis <35601060+my0sotis@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:44:10 +0800 Subject: [PATCH 23/93] fix: ide reconciler convert error (#1534) --- .../daemonpodupdater/daemon_pod_updater_controller.go | 3 ++- pkg/yurtadm/util/initsystem/initsystem_windows.go | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go b/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go index 71a173bab5c..6c06ea36e19 100644 --- a/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go +++ b/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go @@ -173,8 +173,9 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { // 2. Watch for deletion of pods. The reason we watch is that we don't want a daemon set to delete // more pods until all the effects (expectations) of a daemon set's delete have been observed. + updater := r.(*ReconcileDaemonpodupdater) if err := c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.Funcs{ - DeleteFunc: r.(*ReconcileDaemonpodupdater).deletePod, + DeleteFunc: updater.deletePod, }); err != nil { return err } diff --git a/pkg/yurtadm/util/initsystem/initsystem_windows.go b/pkg/yurtadm/util/initsystem/initsystem_windows.go index 6101a2dda51..c45751f4750 100644 --- a/pkg/yurtadm/util/initsystem/initsystem_windows.go +++ b/pkg/yurtadm/util/initsystem/initsystem_windows.go @@ -55,19 +55,19 @@ func (sysd WindowsInitSystem) ServiceIsEnabled(service string) bool { func (sysd WindowsInitSystem) ServiceEnable(service string) error { m, err := mgr.Connect() if err != nil { - return false + return err } defer m.Disconnect() s, err := m.OpenService(service) if err != nil { - return false + return err } defer s.Close() c, err := s.Config() if err != nil { - return false + return err } c.StartType = mgr.StartAutomatic From 3c63cb96a205aafa8e0d08369c21b844c4770040 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:46:10 +0800 Subject: [PATCH 24/93] rename pool-coordinator to yurt-coordinator for yurthub (#1532) --- cmd/yurthub/app/config/config.go | 2 +- cmd/yurthub/app/options/options.go | 12 +-- cmd/yurthub/app/options/options_test.go | 4 +- cmd/yurthub/app/start.go | 38 +++++----- pkg/yurthub/cachemanager/cache_manager.go | 2 +- pkg/yurthub/healthchecker/health_checker.go | 2 +- pkg/yurthub/metrics/metrics.go | 48 ++++++------ pkg/yurthub/proxy/local/local.go | 2 +- pkg/yurthub/proxy/pool/pool.go | 64 ++++++++-------- pkg/yurthub/proxy/proxy.go | 32 ++++---- pkg/yurthub/proxy/remote/loadbalancer.go | 26 +++---- pkg/yurthub/proxy/util/util.go | 2 +- pkg/yurthub/storage/etcd/keycache.go | 4 +- pkg/yurthub/storage/etcd/keycache_test.go | 4 +- pkg/yurthub/storage/etcd/storage.go | 8 +- pkg/yurthub/util/util.go | 18 ++--- .../certmanager/certmanager.go | 24 +++--- .../certmanager/certmanager_test.go | 74 +++++++++---------- .../constants/constants.go | 2 +- .../coordinator.go | 68 ++++++++--------- .../coordinator_test.go | 2 +- .../fake_coordinator.go | 2 +- .../informer_lease.go | 2 +- .../leader_election.go | 2 +- .../resources/resources.go | 0 .../verifiable_pool_scope_resource.go | 0 26 files changed, 222 insertions(+), 222 deletions(-) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/certmanager/certmanager.go (89%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/certmanager/certmanager_test.go (90%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/constants/constants.go (93%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/coordinator.go (93%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/coordinator_test.go (99%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/fake_coordinator.go (97%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/informer_lease.go (99%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/leader_election.go (99%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/resources/resources.go (100%) rename pkg/yurthub/{poolcoordinator => yurtcoordinator}/resources/verifiable_pool_scope_resource.go (100%) diff --git a/cmd/yurthub/app/config/config.go b/cmd/yurthub/app/config/config.go index e89c1600e8a..b209a16118e 100644 --- a/cmd/yurthub/app/config/config.go +++ b/cmd/yurthub/app/config/config.go @@ -170,7 +170,7 @@ func Complete(options *options.YurtHubOptions) (*YurtHubConfiguration, error) { YurtHubNamespace: options.YurtHubNamespace, ProxiedClient: proxiedClient, DiskCachePath: options.DiskCachePath, - CoordinatorPKIDir: filepath.Join(options.RootDir, "poolcoordinator"), + CoordinatorPKIDir: filepath.Join(options.RootDir, "yurtcoordinator"), EnableCoordinator: options.EnableCoordinator, CoordinatorServerURL: coordinatorServerURL, CoordinatorStoragePrefix: options.CoordinatorStoragePrefix, diff --git a/cmd/yurthub/app/options/options.go b/cmd/yurthub/app/options/options.go index 2f3b4eb40f3..ccabdbdd097 100644 --- a/cmd/yurthub/app/options/options.go +++ b/cmd/yurthub/app/options/options.go @@ -120,8 +120,8 @@ func NewYurtHubOptions() *YurtHubOptions { MinRequestTimeout: time.Second * 1800, CACertHashes: make([]string, 0), UnsafeSkipCAVerification: true, - CoordinatorServerAddr: fmt.Sprintf("https://%s:%s", util.DefaultPoolCoordinatorAPIServerSvcName, util.DefaultPoolCoordinatorAPIServerSvcPort), - CoordinatorStorageAddr: fmt.Sprintf("https://%s:%s", util.DefaultPoolCoordinatorEtcdSvcName, util.DefaultPoolCoordinatorEtcdSvcPort), + CoordinatorServerAddr: fmt.Sprintf("https://%s:%s", util.DefaultYurtCoordinatorAPIServerSvcName, util.DefaultYurtCoordinatorAPIServerSvcPort), + CoordinatorStorageAddr: fmt.Sprintf("https://%s:%s", util.DefaultYurtCoordinatorEtcdSvcName, util.DefaultYurtCoordinatorEtcdSvcPort), CoordinatorStoragePrefix: "/registry", LeaderElection: componentbaseconfig.LeaderElectionConfiguration{ LeaderElect: true, @@ -208,17 +208,17 @@ func (o *YurtHubOptions) AddFlags(fs *pflag.FlagSet) { fs.DurationVar(&o.MinRequestTimeout, "min-request-timeout", o.MinRequestTimeout, "An optional field indicating at least how long a proxy handler must keep a request open before timing it out. Currently only honored by the local watch request handler(use request parameter timeoutSeconds firstly), which picks a randomized value above this number as the connection timeout, to spread out load.") fs.StringSliceVar(&o.CACertHashes, "discovery-token-ca-cert-hash", o.CACertHashes, "For token-based discovery, validate that the root CA public key matches this hash (format: \":\").") fs.BoolVar(&o.UnsafeSkipCAVerification, "discovery-token-unsafe-skip-ca-verification", o.UnsafeSkipCAVerification, "For token-based discovery, allow joining without --discovery-token-ca-cert-hash pinning.") - fs.BoolVar(&o.EnableCoordinator, "enable-coordinator", o.EnableCoordinator, "make yurthub aware of the pool coordinator") + fs.BoolVar(&o.EnableCoordinator, "enable-coordinator", o.EnableCoordinator, "make yurthub aware of the yurt coordinator") fs.StringVar(&o.CoordinatorServerAddr, "coordinator-server-addr", o.CoordinatorServerAddr, "Coordinator APIServer address in format https://host:port") - fs.StringVar(&o.CoordinatorStoragePrefix, "coordinator-storage-prefix", o.CoordinatorStoragePrefix, "Pool-Coordinator etcd storage prefix, same as etcd-prefix of Kube-APIServer") - fs.StringVar(&o.CoordinatorStorageAddr, "coordinator-storage-addr", o.CoordinatorStorageAddr, "Address of Pool-Coordinator etcd, in the format host:port") + fs.StringVar(&o.CoordinatorStoragePrefix, "coordinator-storage-prefix", o.CoordinatorStoragePrefix, "Yurt-Coordinator etcd storage prefix, same as etcd-prefix of Kube-APIServer") + fs.StringVar(&o.CoordinatorStorageAddr, "coordinator-storage-addr", o.CoordinatorStorageAddr, "Address of Yurt-Coordinator etcd, in the format host:port") bindFlags(&o.LeaderElection, fs) } // bindFlags binds the LeaderElectionConfiguration struct fields to a flagset func bindFlags(l *componentbaseconfig.LeaderElectionConfiguration, fs *pflag.FlagSet) { fs.BoolVar(&l.LeaderElect, "leader-elect", l.LeaderElect, ""+ - "Start a leader election client and gain leadership based on pool coordinator") + "Start a leader election client and gain leadership based on yurt coordinator") fs.DurationVar(&l.LeaseDuration.Duration, "leader-elect-lease-duration", l.LeaseDuration.Duration, ""+ "The duration that non-leader candidates will wait after observing a leadership "+ "renewal until attempting to acquire leadership of a led but unrenewed leader "+ diff --git a/cmd/yurthub/app/options/options_test.go b/cmd/yurthub/app/options/options_test.go index f9888e35c86..d9d2379b600 100644 --- a/cmd/yurthub/app/options/options_test.go +++ b/cmd/yurthub/app/options/options_test.go @@ -64,8 +64,8 @@ func TestNewYurtHubOptions(t *testing.T) { MinRequestTimeout: time.Second * 1800, CACertHashes: make([]string, 0), UnsafeSkipCAVerification: true, - CoordinatorServerAddr: fmt.Sprintf("https://%s:%s", util.DefaultPoolCoordinatorAPIServerSvcName, util.DefaultPoolCoordinatorAPIServerSvcPort), - CoordinatorStorageAddr: fmt.Sprintf("https://%s:%s", util.DefaultPoolCoordinatorEtcdSvcName, util.DefaultPoolCoordinatorEtcdSvcPort), + CoordinatorServerAddr: fmt.Sprintf("https://%s:%s", util.DefaultYurtCoordinatorAPIServerSvcName, util.DefaultYurtCoordinatorAPIServerSvcPort), + CoordinatorStorageAddr: fmt.Sprintf("https://%s:%s", util.DefaultYurtCoordinatorEtcdSvcName, util.DefaultYurtCoordinatorEtcdSvcPort), CoordinatorStoragePrefix: "/registry", LeaderElection: componentbaseconfig.LeaderElectionConfiguration{ LeaderElect: true, diff --git a/cmd/yurthub/app/start.go b/cmd/yurthub/app/start.go index 649ef863f0f..ba6b166ede8 100644 --- a/cmd/yurthub/app/start.go +++ b/cmd/yurthub/app/start.go @@ -38,13 +38,13 @@ import ( "github.com/openyurtio/openyurt/pkg/yurthub/gc" "github.com/openyurtio/openyurt/pkg/yurthub/healthchecker" hubrest "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/rest" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator" - coordinatorcertmgr "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/certmanager" "github.com/openyurtio/openyurt/pkg/yurthub/proxy" "github.com/openyurtio/openyurt/pkg/yurthub/server" "github.com/openyurtio/openyurt/pkg/yurthub/tenant" "github.com/openyurtio/openyurt/pkg/yurthub/transport" "github.com/openyurtio/openyurt/pkg/yurthub/util" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator" + coordinatorcertmgr "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/certmanager" ) // NewCmdStartYurtHub creates a *cobra.Command object with default parameters @@ -108,7 +108,7 @@ func Run(ctx context.Context, cfg *config.YurtHubConfiguration) error { var cloudHealthChecker healthchecker.MultipleBackendsHealthChecker if cfg.WorkingMode == util.WorkingModeEdge { - klog.Infof("%d. create health checkers for remote servers and pool coordinator", trace) + klog.Infof("%d. create health checkers for remote servers and yurt coordinator", trace) cloudHealthChecker, err = healthchecker.NewCloudAPIServerHealthChecker(cfg, cloudClients, ctx.Done()) if err != nil { return fmt.Errorf("could not new cloud health checker, %w", err) @@ -116,7 +116,7 @@ func Run(ctx context.Context, cfg *config.YurtHubConfiguration) error { } else { klog.Infof("%d. disable health checker for node %s because it is a cloud node", trace, cfg.NodeName) // In cloud mode, cloud health checker is not needed. - // This fake checker will always report that the cloud is healthy and pool coordinator is unhealthy. + // This fake checker will always report that the cloud is healthy and yurt coordinator is unhealthy. cloudHealthChecker = healthchecker.NewFakeChecker(true, make(map[string]int)) } trace++ @@ -155,7 +155,7 @@ func Run(ctx context.Context, cfg *config.YurtHubConfiguration) error { var coordinatorHealthCheckerGetter func() healthchecker.HealthChecker = getFakeCoordinatorHealthChecker var coordinatorTransportManagerGetter func() transport.Interface = getFakeCoordinatorTransportManager - var coordinatorGetter func() poolcoordinator.Coordinator = getFakeCoordinator + var coordinatorGetter func() yurtcoordinator.Coordinator = getFakeCoordinator var coordinatorServerURLGetter func() *url.URL = getFakeCoordinatorServerURL if cfg.EnableCoordinator { @@ -237,12 +237,12 @@ func coordinatorRun(ctx context.Context, coordinatorInformerRegistryChan chan struct{}) ( func() healthchecker.HealthChecker, func() transport.Interface, - func() poolcoordinator.Coordinator, + func() yurtcoordinator.Coordinator, func() *url.URL) { var coordinatorHealthChecker healthchecker.HealthChecker var coordinatorTransportMgr transport.Interface - var coordinator poolcoordinator.Coordinator + var coordinator yurtcoordinator.Coordinator var coordinatorServiceUrl *url.URL go func() { @@ -261,9 +261,9 @@ func coordinatorRun(ctx context.Context, } klog.Info("coordinatorRun sync service complete") - // resolve pool-coordinator-apiserver and etcd from domain to ips + // resolve yurt-coordinator-apiserver and etcd from domain to ips serviceList := cfg.SharedFactory.Core().V1().Services().Lister() - // if pool-coordinator-apiserver and pool-coordinator-etcd address is ip, don't need to resolve + // if yurt-coordinator-apiserver and yurt-coordinator-etcd address is ip, don't need to resolve apiServerIP := net.ParseIP(cfg.CoordinatorServerURL.Hostname()) etcdUrl, err := url.Parse(cfg.CoordinatorStorageAddr) if err != nil { @@ -295,7 +295,7 @@ func coordinatorRun(ctx context.Context, cfg.CoordinatorStorageAddr = fmt.Sprintf("https://%s:%s", etcdService.Spec.ClusterIP, etcdUrl.Port()) } - coorTransportMgr, err := poolCoordinatorTransportMgrGetter(coorCertManager, ctx.Done()) + coorTransportMgr, err := yurtCoordinatorTransportMgrGetter(coorCertManager, ctx.Done()) if err != nil { klog.Errorf("coordinator failed to create coordinator transport manager, %v", err) return @@ -307,7 +307,7 @@ func coordinatorRun(ctx context.Context, Timeout: time.Duration(cfg.HeartbeatTimeoutSeconds) * time.Second, }) if err != nil { - klog.Errorf("coordinator failed to get coordinator client for pool coordinator, %v", err) + klog.Errorf("coordinator failed to get coordinator client for yurt coordinator, %v", err) return } @@ -317,15 +317,15 @@ func coordinatorRun(ctx context.Context, return } - var elector *poolcoordinator.HubElector - elector, err = poolcoordinator.NewHubElector(cfg, coordinatorClient, coorHealthChecker, cloudHealthChecker, ctx.Done()) + var elector *yurtcoordinator.HubElector + elector, err = yurtcoordinator.NewHubElector(cfg, coordinatorClient, coorHealthChecker, cloudHealthChecker, ctx.Done()) if err != nil { klog.Errorf("coordinator failed to create hub elector, %v", err) return } go elector.Run(ctx.Done()) - coor, err := poolcoordinator.NewCoordinator(ctx, cfg, cloudHealthChecker, restConfigMgr, coorCertManager, coorTransportMgr, elector) + coor, err := yurtcoordinator.NewCoordinator(ctx, cfg, cloudHealthChecker, restConfigMgr, coorCertManager, coorTransportMgr, elector) if err != nil { klog.Errorf("coordinator failed to create coordinator, %v", err) return @@ -342,14 +342,14 @@ func coordinatorRun(ctx context.Context, return coordinatorHealthChecker }, func() transport.Interface { return coordinatorTransportMgr - }, func() poolcoordinator.Coordinator { + }, func() yurtcoordinator.Coordinator { return coordinator }, func() *url.URL { return coordinatorServiceUrl } } -func poolCoordinatorTransportMgrGetter(coordinatorCertMgr *coordinatorcertmgr.CertManager, stopCh <-chan struct{}) (transport.Interface, error) { +func yurtCoordinatorTransportMgrGetter(coordinatorCertMgr *coordinatorcertmgr.CertManager, stopCh <-chan struct{}) (transport.Interface, error) { err := wait.PollImmediate(5*time.Second, 4*time.Minute, func() (done bool, err error) { klog.Info("waiting for preparing certificates for coordinator client and node lease proxy client") if coordinatorCertMgr.GetAPIServerClientCert() == nil { @@ -366,13 +366,13 @@ func poolCoordinatorTransportMgrGetter(coordinatorCertMgr *coordinatorcertmgr.Ce coordinatorTransportMgr, err := transport.NewTransportManager(coordinatorCertMgr, stopCh) if err != nil { - return nil, fmt.Errorf("failed to create transport manager for pool coordinator, %v", err) + return nil, fmt.Errorf("failed to create transport manager for yurt coordinator, %v", err) } return coordinatorTransportMgr, nil } -func getFakeCoordinator() poolcoordinator.Coordinator { - return &poolcoordinator.FakeCoordinator{} +func getFakeCoordinator() yurtcoordinator.Coordinator { + return &yurtcoordinator.FakeCoordinator{} } func getFakeCoordinatorHealthChecker() healthchecker.HealthChecker { diff --git a/pkg/yurthub/cachemanager/cache_manager.go b/pkg/yurthub/cachemanager/cache_manager.go index 1d1769582e8..7e73ce84796 100644 --- a/pkg/yurthub/cachemanager/cache_manager.go +++ b/pkg/yurthub/cachemanager/cache_manager.go @@ -268,7 +268,7 @@ func (cm *cacheManager) queryOneObject(req *http.Request) (runtime.Object, error // Note: // When cloud-edge network is healthy, the inMemoryCache can be updated with response from cloud side. // While cloud-edge network is broken, the inMemoryCache can only be full filled with data from edge cache, - // such as local disk and pool-coordinator. + // such as local disk and yurt-coordinator. if isInMemoryCacheMiss { if inMemoryCacheKey, err := inMemoryCacheKeyFunc(info); err != nil { klog.Errorf("cannot in-memory cache key for req %s, %v", util.ReqString(req), err) diff --git a/pkg/yurthub/healthchecker/health_checker.go b/pkg/yurthub/healthchecker/health_checker.go index 5eaa66fa8ab..a70a5699309 100644 --- a/pkg/yurthub/healthchecker/health_checker.go +++ b/pkg/yurthub/healthchecker/health_checker.go @@ -58,7 +58,7 @@ type coordinatorHealthChecker struct { heartbeatInterval int } -// NewCoordinatorHealthChecker returns a health checker for verifying pool coordinator status. +// NewCoordinatorHealthChecker returns a health checker for verifying yurt coordinator status. func NewCoordinatorHealthChecker(cfg *config.YurtHubConfiguration, checkerClient kubernetes.Interface, cloudServerHealthChecker HealthChecker, stopCh <-chan struct{}) (HealthChecker, error) { chc := &coordinatorHealthChecker{ cloudServerHealthChecker: cloudServerHealthChecker, diff --git a/pkg/yurthub/metrics/metrics.go b/pkg/yurthub/metrics/metrics.go index 79e95c52912..49e4ecd38ba 100644 --- a/pkg/yurthub/metrics/metrics.go +++ b/pkg/yurthub/metrics/metrics.go @@ -51,9 +51,9 @@ type HubMetrics struct { closableConnsCollector *prometheus.GaugeVec proxyTrafficCollector *prometheus.CounterVec proxyLatencyCollector *prometheus.GaugeVec - poolCoordinatorYurthubRoleCollector *prometheus.GaugeVec - poolCoordinatorHealthyStatusCollector *prometheus.GaugeVec - poolCoordinatorReadyStatusCollector *prometheus.GaugeVec + yurtCoordinatorYurthubRoleCollector *prometheus.GaugeVec + yurtCoordinatorHealthyStatusCollector *prometheus.GaugeVec + yurtCoordinatorReadyStatusCollector *prometheus.GaugeVec } func newHubMetrics() *HubMetrics { @@ -111,28 +111,28 @@ func newHubMetrics() *HubMetrics { Help: "collector of proxy latency of incoming requests(unit: ms)", }, []string{"client", "verb", "resource", "subresources", "type"}) - poolCoordinatorYurthubRoleCollector := prometheus.NewGaugeVec( + yurtCoordinatorYurthubRoleCollector := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: subsystem, - Name: "pool_coordinator_yurthub_role", - Help: "pool coordinator status of yurthub. 1: LeaderHub, 2: FollowerHub 3: Pending", + Name: "yurt_coordinator_yurthub_role", + Help: "yurt coordinator status of yurthub. 1: LeaderHub, 2: FollowerHub 3: Pending", }, []string{}) - poolCoordinatorHealthyStatusCollector := prometheus.NewGaugeVec( + yurtCoordinatorHealthyStatusCollector := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: subsystem, - Name: "pool_coordinator_healthy_status", - Help: "pool coordinator heahty status 1: healthy, 0: unhealthy", + Name: "yurt_coordinator_healthy_status", + Help: "yurt coordinator heahty status 1: healthy, 0: unhealthy", }, []string{}) - poolCoordinatorReadyStatusCollector := prometheus.NewGaugeVec( + yurtCoordinatorReadyStatusCollector := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: namespace, Subsystem: subsystem, - Name: "pool_coordinator_ready_status", - Help: "pool coordinator ready status 1: ready, 0: notReady", + Name: "yurt_coordinator_ready_status", + Help: "yurt coordinator ready status 1: ready, 0: notReady", }, []string{}) prometheus.MustRegister(serversHealthyCollector) @@ -142,9 +142,9 @@ func newHubMetrics() *HubMetrics { prometheus.MustRegister(closableConnsCollector) prometheus.MustRegister(proxyTrafficCollector) prometheus.MustRegister(proxyLatencyCollector) - prometheus.MustRegister(poolCoordinatorYurthubRoleCollector) - prometheus.MustRegister(poolCoordinatorHealthyStatusCollector) - prometheus.MustRegister(poolCoordinatorReadyStatusCollector) + prometheus.MustRegister(yurtCoordinatorYurthubRoleCollector) + prometheus.MustRegister(yurtCoordinatorHealthyStatusCollector) + prometheus.MustRegister(yurtCoordinatorReadyStatusCollector) return &HubMetrics{ serversHealthyCollector: serversHealthyCollector, inFlightRequestsCollector: inFlightRequestsCollector, @@ -153,9 +153,9 @@ func newHubMetrics() *HubMetrics { closableConnsCollector: closableConnsCollector, proxyTrafficCollector: proxyTrafficCollector, proxyLatencyCollector: proxyLatencyCollector, - poolCoordinatorHealthyStatusCollector: poolCoordinatorHealthyStatusCollector, - poolCoordinatorReadyStatusCollector: poolCoordinatorReadyStatusCollector, - poolCoordinatorYurthubRoleCollector: poolCoordinatorYurthubRoleCollector, + yurtCoordinatorHealthyStatusCollector: yurtCoordinatorHealthyStatusCollector, + yurtCoordinatorReadyStatusCollector: yurtCoordinatorReadyStatusCollector, + yurtCoordinatorYurthubRoleCollector: yurtCoordinatorYurthubRoleCollector, } } @@ -172,16 +172,16 @@ func (hm *HubMetrics) ObserveServerHealthy(server string, status int) { hm.serversHealthyCollector.WithLabelValues(server).Set(float64(status)) } -func (hm *HubMetrics) ObservePoolCoordinatorYurthubRole(status int32) { - hm.poolCoordinatorYurthubRoleCollector.WithLabelValues().Set(float64(status)) +func (hm *HubMetrics) ObserveYurtCoordinatorYurthubRole(status int32) { + hm.yurtCoordinatorYurthubRoleCollector.WithLabelValues().Set(float64(status)) } -func (hm *HubMetrics) ObservePoolCoordinatorReadyStatus(status int32) { - hm.poolCoordinatorReadyStatusCollector.WithLabelValues().Set(float64(status)) +func (hm *HubMetrics) ObserveYurtCoordinatorReadyStatus(status int32) { + hm.yurtCoordinatorReadyStatusCollector.WithLabelValues().Set(float64(status)) } -func (hm *HubMetrics) ObservePoolCoordinatorHealthyStatus(status int32) { - hm.poolCoordinatorHealthyStatusCollector.WithLabelValues().Set(float64(status)) +func (hm *HubMetrics) ObserveYurtCoordinatorHealthyStatus(status int32) { + hm.yurtCoordinatorHealthyStatusCollector.WithLabelValues().Set(float64(status)) } func (hm *HubMetrics) IncInFlightRequests(verb, resource, subresource, client string) { diff --git a/pkg/yurthub/proxy/local/local.go b/pkg/yurthub/proxy/local/local.go index 46b7fb30c9f..8e1304b3bf9 100644 --- a/pkg/yurthub/proxy/local/local.go +++ b/pkg/yurthub/proxy/local/local.go @@ -204,7 +204,7 @@ func (lp *LocalProxy) localWatch(w http.ResponseWriter, req *http.Request) error return nil } - // if poolcoordinator becomes healthy, exit the watch wait + // if yurtcoordinator becomes healthy, exit the watch wait if isPoolScopedListWatch && lp.isCoordinatorReady() { return nil } diff --git a/pkg/yurthub/proxy/pool/pool.go b/pkg/yurthub/proxy/pool/pool.go index e631d475f8a..8e6de82a7ae 100644 --- a/pkg/yurthub/proxy/pool/pool.go +++ b/pkg/yurthub/proxy/pool/pool.go @@ -40,24 +40,24 @@ const ( watchCheckInterval = 5 * time.Second ) -// PoolCoordinatorProxy is responsible for handling requests when remote servers are unhealthy -type PoolCoordinatorProxy struct { - poolCoordinatorProxy *util.RemoteProxy +// YurtCoordinatorProxy is responsible for handling requests when remote servers are unhealthy +type YurtCoordinatorProxy struct { + yurtCoordinatorProxy *util.RemoteProxy localCacheMgr cachemanager.CacheManager filterMgr *manager.Manager isCoordinatorReady func() bool stopCh <-chan struct{} } -func NewPoolCoordinatorProxy( +func NewYurtCoordinatorProxy( localCacheMgr cachemanager.CacheManager, transportMgrGetter func() transport.Interface, coordinatorServerURLGetter func() *url.URL, filterMgr *manager.Manager, isCoordinatorReady func() bool, - stopCh <-chan struct{}) (*PoolCoordinatorProxy, error) { + stopCh <-chan struct{}) (*YurtCoordinatorProxy, error) { - pp := &PoolCoordinatorProxy{ + pp := &YurtCoordinatorProxy{ localCacheMgr: localCacheMgr, isCoordinatorReady: isCoordinatorReady, filterMgr: filterMgr, @@ -86,12 +86,12 @@ func NewPoolCoordinatorProxy( transportMgr, stopCh) if err != nil { - klog.Errorf("failed to create remote proxy for pool-coordinator, %v", err) + klog.Errorf("failed to create remote proxy for yurt-coordinator, %v", err) return } - pp.poolCoordinatorProxy = proxy - klog.Infof("create remote proxy for pool-coordinator success, coordinatorServerURL: %s", coordinatorServerURL.String()) + pp.yurtCoordinatorProxy = proxy + klog.Infof("create remote proxy for yurt-coordinator success, coordinatorServerURL: %s", coordinatorServerURL.String()) return } } @@ -100,16 +100,16 @@ func NewPoolCoordinatorProxy( return pp, nil } -// ServeHTTP of PoolCoordinatorProxy is able to handle read-only request, including +// ServeHTTP of YurtCoordinatorProxy is able to handle read-only request, including // watch, list, get. Other verbs that will write data to the cache are not supported // currently. -func (pp *PoolCoordinatorProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { +func (pp *YurtCoordinatorProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { var err error ctx := req.Context() reqInfo, ok := apirequest.RequestInfoFrom(ctx) if !ok || reqInfo == nil { - klog.Errorf("pool-coordinator proxy cannot handle request(%s), cannot get requestInfo", hubutil.ReqString(req), reqInfo) - util.Err(errors.NewBadRequest(fmt.Sprintf("pool-coordinator proxy cannot handle request(%s), cannot get requestInfo", hubutil.ReqString(req))), rw, req) + klog.Errorf("yurt-coordinator proxy cannot handle request(%s), cannot get requestInfo", hubutil.ReqString(req), reqInfo) + util.Err(errors.NewBadRequest(fmt.Sprintf("yurt-coordinator proxy cannot handle request(%s), cannot get requestInfo", hubutil.ReqString(req))), rw, req) return } req.Header.Del("Authorization") // delete token with cloud apiServer RBAC and use yurthub authorization @@ -122,41 +122,41 @@ func (pp *PoolCoordinatorProxy) ServeHTTP(rw http.ResponseWriter, req *http.Requ case "watch": err = pp.poolWatch(rw, req) default: - err = fmt.Errorf("unsupported verb for pool coordinator proxy: %s", reqInfo.Verb) + err = fmt.Errorf("unsupported verb for yurt coordinator proxy: %s", reqInfo.Verb) } if err != nil { - klog.Errorf("could not proxy to pool-coordinator for %s, %v", hubutil.ReqString(req), err) + klog.Errorf("could not proxy to yurt-coordinator for %s, %v", hubutil.ReqString(req), err) util.Err(errors.NewBadRequest(err.Error()), rw, req) } } else { - klog.Errorf("pool-coordinator does not support request(%s), requestInfo: %s", hubutil.ReqString(req), hubutil.ReqInfoString(reqInfo)) - util.Err(errors.NewBadRequest(fmt.Sprintf("pool-coordinator does not support request(%s)", hubutil.ReqString(req))), rw, req) + klog.Errorf("yurt-coordinator does not support request(%s), requestInfo: %s", hubutil.ReqString(req), hubutil.ReqInfoString(reqInfo)) + util.Err(errors.NewBadRequest(fmt.Sprintf("yurt-coordinator does not support request(%s)", hubutil.ReqString(req))), rw, req) } } -func (pp *PoolCoordinatorProxy) poolPost(rw http.ResponseWriter, req *http.Request) error { +func (pp *YurtCoordinatorProxy) poolPost(rw http.ResponseWriter, req *http.Request) error { ctx := req.Context() info, _ := apirequest.RequestInfoFrom(ctx) klog.V(4).Infof("pool handle post, req=%s, reqInfo=%s", hubutil.ReqString(req), hubutil.ReqInfoString(info)) - if (util.IsSubjectAccessReviewCreateGetRequest(req) || util.IsEventCreateRequest(req)) && pp.poolCoordinatorProxy != nil { + if (util.IsSubjectAccessReviewCreateGetRequest(req) || util.IsEventCreateRequest(req)) && pp.yurtCoordinatorProxy != nil { // kubelet needs to create subjectaccessreviews for auth - pp.poolCoordinatorProxy.ServeHTTP(rw, req) + pp.yurtCoordinatorProxy.ServeHTTP(rw, req) return nil } return fmt.Errorf("unsupported post request") } -func (pp *PoolCoordinatorProxy) poolQuery(rw http.ResponseWriter, req *http.Request) error { - if (util.IsPoolScopedResouceListWatchRequest(req) || util.IsSubjectAccessReviewCreateGetRequest(req)) && pp.poolCoordinatorProxy != nil { - pp.poolCoordinatorProxy.ServeHTTP(rw, req) +func (pp *YurtCoordinatorProxy) poolQuery(rw http.ResponseWriter, req *http.Request) error { + if (util.IsPoolScopedResouceListWatchRequest(req) || util.IsSubjectAccessReviewCreateGetRequest(req)) && pp.yurtCoordinatorProxy != nil { + pp.yurtCoordinatorProxy.ServeHTTP(rw, req) return nil } return fmt.Errorf("unsupported query request") } -func (pp *PoolCoordinatorProxy) poolWatch(rw http.ResponseWriter, req *http.Request) error { - if util.IsPoolScopedResouceListWatchRequest(req) && pp.poolCoordinatorProxy != nil { +func (pp *YurtCoordinatorProxy) poolWatch(rw http.ResponseWriter, req *http.Request) error { + if util.IsPoolScopedResouceListWatchRequest(req) && pp.yurtCoordinatorProxy != nil { clientReqCtx := req.Context() poolServeCtx, poolServeCancel := context.WithCancel(clientReqCtx) @@ -167,27 +167,27 @@ func (pp *PoolCoordinatorProxy) poolWatch(rw http.ResponseWriter, req *http.Requ select { case <-t.C: if !pp.isCoordinatorReady() { - klog.Infof("notified the pool coordinator is not ready for handling request, cancel watch %s", hubutil.ReqString(req)) + klog.Infof("notified the yurt coordinator is not ready for handling request, cancel watch %s", hubutil.ReqString(req)) util.ReListWatchReq(rw, req) poolServeCancel() return } case <-clientReqCtx.Done(): - klog.Infof("notified client canceled the watch request %s, stop proxy it to pool coordinator", hubutil.ReqString(req)) + klog.Infof("notified client canceled the watch request %s, stop proxy it to yurt coordinator", hubutil.ReqString(req)) return } } }() newReq := req.Clone(poolServeCtx) - pp.poolCoordinatorProxy.ServeHTTP(rw, newReq) - klog.Infof("watch %s to pool coordinator exited", hubutil.ReqString(req)) + pp.yurtCoordinatorProxy.ServeHTTP(rw, newReq) + klog.Infof("watch %s to yurt coordinator exited", hubutil.ReqString(req)) return nil } return fmt.Errorf("unsupported watch request") } -func (pp *PoolCoordinatorProxy) errorHandler(rw http.ResponseWriter, req *http.Request, err error) { +func (pp *YurtCoordinatorProxy) errorHandler(rw http.ResponseWriter, req *http.Request, err error) { klog.Errorf("remote proxy error handler: %s, %v", hubutil.ReqString(req), err) ctx := req.Context() if info, ok := apirequest.RequestInfoFrom(ctx); ok { @@ -201,7 +201,7 @@ func (pp *PoolCoordinatorProxy) errorHandler(rw http.ResponseWriter, req *http.R rw.WriteHeader(http.StatusBadGateway) } -func (pp *PoolCoordinatorProxy) modifyResponse(resp *http.Response) error { +func (pp *YurtCoordinatorProxy) modifyResponse(resp *http.Response) error { if resp == nil || resp.Request == nil { klog.Info("no request info in response, skip cache response") return nil @@ -261,7 +261,7 @@ func (pp *PoolCoordinatorProxy) modifyResponse(resp *http.Response) error { return nil } -func (pp *PoolCoordinatorProxy) cacheResponse(req *http.Request, resp *http.Response) { +func (pp *YurtCoordinatorProxy) cacheResponse(req *http.Request, resp *http.Response) { if pp.localCacheMgr.CanCacheFor(req) { ctx := req.Context() req = req.WithContext(ctx) diff --git a/pkg/yurthub/proxy/proxy.go b/pkg/yurthub/proxy/proxy.go index 1de3f9a8108..5a6a5e0c4c8 100644 --- a/pkg/yurthub/proxy/proxy.go +++ b/pkg/yurthub/proxy/proxy.go @@ -37,8 +37,6 @@ import ( "github.com/openyurtio/openyurt/cmd/yurthub/app/config" "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/healthchecker" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator" - coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/constants" "github.com/openyurtio/openyurt/pkg/yurthub/proxy/local" "github.com/openyurtio/openyurt/pkg/yurthub/proxy/pool" "github.com/openyurtio/openyurt/pkg/yurthub/proxy/remote" @@ -46,6 +44,8 @@ import ( "github.com/openyurtio/openyurt/pkg/yurthub/tenant" "github.com/openyurtio/openyurt/pkg/yurthub/transport" hubutil "github.com/openyurtio/openyurt/pkg/yurthub/util" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator" + coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/constants" ) type yurtReverseProxy struct { @@ -59,7 +59,7 @@ type yurtReverseProxy struct { tenantMgr tenant.Interface isCoordinatorReady func() bool workingMode hubutil.WorkingMode - enablePoolCoordinator bool + enableYurtCoordinator bool } // NewYurtReverseProxyHandler creates a http handler for proxying @@ -70,7 +70,7 @@ func NewYurtReverseProxyHandler( transportMgr transport.Interface, cloudHealthChecker healthchecker.MultipleBackendsHealthChecker, tenantMgr tenant.Interface, - coordinatorGetter func() poolcoordinator.Coordinator, + coordinatorGetter func() yurtcoordinator.Coordinator, coordinatorTransportMgrGetter func() transport.Interface, coordinatorHealthCheckerGetter func() healthchecker.HealthChecker, coordinatorServerURLGetter func() *url.URL, @@ -123,7 +123,7 @@ func NewYurtReverseProxyHandler( localProxy = local.WithFakeTokenInject(localProxy, yurtHubCfg.SerializerManager) if yurtHubCfg.EnableCoordinator { - poolProxy, err = pool.NewPoolCoordinatorProxy( + poolProxy, err = pool.NewYurtCoordinatorProxy( localCacheMgr, coordinatorTransportMgrGetter, coordinatorServerURLGetter, @@ -145,7 +145,7 @@ func NewYurtReverseProxyHandler( poolProxy: poolProxy, maxRequestsInFlight: yurtHubCfg.MaxRequestInFlight, isCoordinatorReady: isCoordinatorReady, - enablePoolCoordinator: yurtHubCfg.EnableCoordinator, + enableYurtCoordinator: yurtHubCfg.EnableCoordinator, tenantMgr: tenantMgr, workingMode: yurtHubCfg.WorkingMode, } @@ -167,7 +167,7 @@ func (p *yurtReverseProxy) buildHandlerChain(handler http.Handler) http.Handler handler = util.WithMaxInFlightLimit(handler, p.maxRequestsInFlight) handler = util.WithRequestClientComponent(handler) - if p.enablePoolCoordinator { + if p.enableYurtCoordinator { handler = util.WithIfPoolScopedResource(handler) } @@ -198,7 +198,7 @@ func (p *yurtReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) case util.IsSubjectAccessReviewCreateGetRequest(req): p.subjectAccessReviewHandler(rw, req) default: - // For resource request that do not need to be handled by pool-coordinator, + // For resource request that do not need to be handled by yurt-coordinator, // handling the request with cloud apiserver or local cache. if p.cloudHealthChecker.IsHealthy() { p.loadBalancer.ServeHTTP(rw, req) @@ -223,7 +223,7 @@ func (p *yurtReverseProxy) handleKubeletLease(rw http.ResponseWriter, req *http. func (p *yurtReverseProxy) eventHandler(rw http.ResponseWriter, req *http.Request) { if p.cloudHealthChecker.IsHealthy() { p.loadBalancer.ServeHTTP(rw, req) - // TODO: We should also consider create the event in pool-coordinator when the cloud is healthy. + // TODO: We should also consider create the event in yurt-coordinator when the cloud is healthy. } else if p.isCoordinatorReady() && p.poolProxy != nil { p.poolProxy.ServeHTTP(rw, req) } else { @@ -251,14 +251,14 @@ func (p *yurtReverseProxy) poolScopedResouceHandler(rw http.ResponseWriter, req } func (p *yurtReverseProxy) subjectAccessReviewHandler(rw http.ResponseWriter, req *http.Request) { - if isSubjectAccessReviewFromPoolCoordinator(req) { - // check if the logs/exec request is from APIServer or PoolCoordinator. - // We should avoid sending SubjectAccessReview to Pool-Coordinator if the logs/exec requests + if isSubjectAccessReviewFromYurtCoordinator(req) { + // check if the logs/exec request is from APIServer or YurtCoordinator. + // We should avoid sending SubjectAccessReview to Yurt-Coordinator if the logs/exec requests // come from APIServer, which may fail for RBAC differences, vise versa. if p.isCoordinatorReady() { p.poolProxy.ServeHTTP(rw, req) } else { - err := errors.New("request is from pool-coordinator but it's currently not healthy") + err := errors.New("request is from yurt-coordinator but it's currently not healthy") klog.Errorf("could not handle SubjectAccessReview req %s, %v", hubutil.ReqString(req), err) util.Err(err, rw, req) } @@ -273,7 +273,7 @@ func (p *yurtReverseProxy) subjectAccessReviewHandler(rw http.ResponseWriter, re } } -func isSubjectAccessReviewFromPoolCoordinator(req *http.Request) bool { +func isSubjectAccessReviewFromYurtCoordinator(req *http.Request) bool { var buf bytes.Buffer if n, err := buf.ReadFrom(req.Body); err != nil || n == 0 { klog.Errorf("failed to read SubjectAccessReview from request %s, read %d bytes, %v", hubutil.ReqString(req), n, err) @@ -299,12 +299,12 @@ func isSubjectAccessReviewFromPoolCoordinator(req *http.Request) bool { sav := got.(*v1.SubjectAccessReview) for _, g := range sav.Spec.Groups { - if g == "openyurt:pool-coordinator" { + if g == "openyurt:yurt-coordinator" { return true } } - klog.V(4).Infof("SubjectAccessReview in request %s is not for pool-coordinator, whose group: %s, user: %s", + klog.V(4).Infof("SubjectAccessReview in request %s is not for yurt-coordinator, whose group: %s, user: %s", hubutil.ReqString(req), strings.Join(sav.Spec.Groups, ";"), sav.Spec.User) return false } diff --git a/pkg/yurthub/proxy/remote/loadbalancer.go b/pkg/yurthub/proxy/remote/loadbalancer.go index a040dea5124..a8aeda01ea2 100644 --- a/pkg/yurthub/proxy/remote/loadbalancer.go +++ b/pkg/yurthub/proxy/remote/loadbalancer.go @@ -33,11 +33,11 @@ import ( "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/filter/manager" "github.com/openyurtio/openyurt/pkg/yurthub/healthchecker" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator" - coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/constants" "github.com/openyurtio/openyurt/pkg/yurthub/proxy/util" "github.com/openyurtio/openyurt/pkg/yurthub/transport" hubutil "github.com/openyurtio/openyurt/pkg/yurthub/util" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator" + coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/constants" ) const ( @@ -133,7 +133,7 @@ type loadBalancer struct { algo loadBalancerAlgo localCacheMgr cachemanager.CacheManager filterManager *manager.Manager - coordinatorGetter func() poolcoordinator.Coordinator + coordinatorGetter func() yurtcoordinator.Coordinator workingMode hubutil.WorkingMode stopCh <-chan struct{} } @@ -144,7 +144,7 @@ func NewLoadBalancer( remoteServers []*url.URL, localCacheMgr cachemanager.CacheManager, transportMgr transport.Interface, - coordinatorGetter func() poolcoordinator.Coordinator, + coordinatorGetter func() yurtcoordinator.Coordinator, healthChecker healthchecker.MultipleBackendsHealthChecker, filterManager *manager.Manager, workingMode hubutil.WorkingMode, @@ -198,11 +198,11 @@ func (lb *loadBalancer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // If pool-scoped resource request is from leader-yurthub, it should always be sent to the cloud APIServer. // Thus we do not need to start a check routine for it. But for other requests, we need to periodically check - // the pool-coordinator status, and switch the traffic to pool-coordinator if it is ready. + // the yurt-coordinator status, and switch the traffic to yurt-coordinator if it is ready. if util.IsPoolScopedResouceListWatchRequest(req) && !isRequestFromLeaderYurthub(req) { - // We get here possibly because the pool-coordinator is not ready. - // We should cancel the watch request when pool-coordinator becomes ready. - klog.Infof("pool-coordinator is not ready, we use cloud APIServer to temporarily handle the req: %s", hubutil.ReqString(req)) + // We get here possibly because the yurt-coordinator is not ready. + // We should cancel the watch request when yurt-coordinator becomes ready. + klog.Infof("yurt-coordinator is not ready, we use cloud APIServer to temporarily handle the req: %s", hubutil.ReqString(req)) clientReqCtx := req.Context() cloudServeCtx, cloudServeCancel := context.WithCancel(clientReqCtx) @@ -217,13 +217,13 @@ func (lb *loadBalancer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { continue } if _, isReady := coordinator.IsReady(); isReady { - klog.Infof("notified the pool coordinator is ready, cancel the req %s making it handled by pool coordinator", hubutil.ReqString(req)) + klog.Infof("notified the yurt coordinator is ready, cancel the req %s making it handled by yurt coordinator", hubutil.ReqString(req)) util.ReListWatchReq(rw, req) cloudServeCancel() return } case <-clientReqCtx.Done(): - klog.Infof("watch req %s is canceled by client, when pool coordinator is not ready", hubutil.ReqString(req)) + klog.Infof("watch req %s is canceled by client, when yurt coordinator is not ready", hubutil.ReqString(req)) return } } @@ -356,9 +356,9 @@ func (lb *loadBalancer) cacheResponse(req *http.Request, resp *http.Response) { if !isLeaderHubUserAgent(ctx) { if isRequestOfNodeAndPod(ctx) { // Currently, for request that does not come from "leader-yurthub", - // we only cache pod and node resources to pool-coordinator. + // we only cache pod and node resources to yurt-coordinator. // Note: We do not allow the non-leader yurthub to cache pool-scoped resources - // into pool-coordinator to ensure that only one yurthub can update pool-scoped + // into yurt-coordinator to ensure that only one yurthub can update pool-scoped // cache to avoid inconsistency of data. lb.cacheToLocalAndPool(req, resp, poolCacheManager) } else { @@ -376,7 +376,7 @@ func (lb *loadBalancer) cacheResponse(req *http.Request, resp *http.Response) { return } - // When pool-coordinator is not healthy or not be enabled, we can + // When yurt-coordinator is not healthy or not be enabled, we can // only cache the response at local. lb.cacheToLocal(req, resp) } diff --git a/pkg/yurthub/proxy/util/util.go b/pkg/yurthub/proxy/util/util.go index d7191188da0..b5f9898e856 100644 --- a/pkg/yurthub/proxy/util/util.go +++ b/pkg/yurthub/proxy/util/util.go @@ -43,9 +43,9 @@ import ( "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer" "github.com/openyurtio/openyurt/pkg/yurthub/metrics" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/resources" "github.com/openyurtio/openyurt/pkg/yurthub/tenant" "github.com/openyurtio/openyurt/pkg/yurthub/util" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/resources" ) const ( diff --git a/pkg/yurthub/storage/etcd/keycache.go b/pkg/yurthub/storage/etcd/keycache.go index 28a76515fe3..ac14ee727c1 100644 --- a/pkg/yurthub/storage/etcd/keycache.go +++ b/pkg/yurthub/storage/etcd/keycache.go @@ -26,9 +26,9 @@ import ( clientv3 "go.etcd.io/etcd/client/v3" "k8s.io/apimachinery/pkg/runtime/schema" - coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/constants" "github.com/openyurtio/openyurt/pkg/yurthub/storage" "github.com/openyurtio/openyurt/pkg/yurthub/util/fs" + coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/constants" ) type storageKeySet map[storageKey]struct{} @@ -99,7 +99,7 @@ func (c *componentKeyCache) Recover() error { return fmt.Errorf("failed to get pool-scoped keys, %v", err) } // Overwrite the data we recovered from local disk, if any. Because we - // only respect to the resources stored in pool-coordinator to recover the + // only respect to the resources stored in yurt-coordinator to recover the // pool-scoped keys. c.cache[coordinatorconstants.DefaultPoolScopedUserAgent] = *poolScopedKeyset diff --git a/pkg/yurthub/storage/etcd/keycache_test.go b/pkg/yurthub/storage/etcd/keycache_test.go index 6ee556eb372..4b08fbf0458 100644 --- a/pkg/yurthub/storage/etcd/keycache_test.go +++ b/pkg/yurthub/storage/etcd/keycache_test.go @@ -28,13 +28,13 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" - mvccpb "go.etcd.io/etcd/api/v3/mvccpb" + "go.etcd.io/etcd/api/v3/mvccpb" clientv3 "go.etcd.io/etcd/client/v3" "k8s.io/apimachinery/pkg/runtime/schema" - coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/constants" etcdmock "github.com/openyurtio/openyurt/pkg/yurthub/storage/etcd/mock" "github.com/openyurtio/openyurt/pkg/yurthub/util/fs" + coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/constants" ) var ( diff --git a/pkg/yurthub/storage/etcd/storage.go b/pkg/yurthub/storage/etcd/storage.go index 61fc10a4fb7..39553a8708b 100644 --- a/pkg/yurthub/storage/etcd/storage.go +++ b/pkg/yurthub/storage/etcd/storage.go @@ -32,14 +32,14 @@ import ( "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/resources" "github.com/openyurtio/openyurt/pkg/yurthub/storage" "github.com/openyurtio/openyurt/pkg/yurthub/storage/utils" "github.com/openyurtio/openyurt/pkg/yurthub/util/fs" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/resources" ) const ( - StorageName = "pool-coordinator" + StorageName = "yurt-coordinator" defaultTimeout = 5 * time.Second defaultHealthCheckPeriod = 10 * time.Second defaultDialTimeout = 10 * time.Second @@ -83,11 +83,11 @@ type etcdStorage struct { // 2. special resources: which are only used by this nodes // In local cache, we do not need to bother to distinguish these two kinds. // For special resources, this node absolutely can create/update/delete them. - // For common resources, thanks to list/watch we can ensure that resources in pool-coordinator + // For common resources, thanks to list/watch we can ensure that resources in yurt-coordinator // are finally consistent with the cloud, though there maybe a little jitter. localComponentKeyCache *componentKeyCache // For etcd storage, we do not need to cache cluster info, because - // we can get it form apiserver in pool-coordinator. + // we can get it form apiserver in yurt-coordinator. doNothingAboutClusterInfo } diff --git a/pkg/yurthub/util/util.go b/pkg/yurthub/util/util.go index 473b856bbac..f4c6df880e5 100644 --- a/pkg/yurthub/util/util.go +++ b/pkg/yurthub/util/util.go @@ -35,7 +35,7 @@ import ( "github.com/openyurtio/openyurt/pkg/projectinfo" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer" "github.com/openyurtio/openyurt/pkg/yurthub/metrics" - coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/constants" + coordinatorconstants "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/constants" ) // ProxyKeyType represents the key in proxy request context @@ -62,14 +62,14 @@ const ( ProxyListSelector // ProxyPoolScopedResource represents if this request is asking for pool-scoped resources ProxyPoolScopedResource - // DefaultPoolCoordinatorEtcdSvcName represents default pool coordinator etcd service - DefaultPoolCoordinatorEtcdSvcName = "pool-coordinator-etcd" - // DefaultPoolCoordinatorAPIServerSvcName represents default pool coordinator apiServer service - DefaultPoolCoordinatorAPIServerSvcName = "pool-coordinator-apiserver" - // DefaultPoolCoordinatorEtcdSvcPort represents default pool coordinator etcd port - DefaultPoolCoordinatorEtcdSvcPort = "2379" - // DefaultPoolCoordinatorAPIServerSvcPort represents default pool coordinator apiServer port - DefaultPoolCoordinatorAPIServerSvcPort = "443" + // DefaultYurtCoordinatorEtcdSvcName represents default yurt coordinator etcd service + DefaultYurtCoordinatorEtcdSvcName = "yurt-coordinator-etcd" + // DefaultYurtCoordinatorAPIServerSvcName represents default yurt coordinator apiServer service + DefaultYurtCoordinatorAPIServerSvcName = "yurt-coordinator-apiserver" + // DefaultYurtCoordinatorEtcdSvcPort represents default yurt coordinator etcd port + DefaultYurtCoordinatorEtcdSvcPort = "2379" + // DefaultYurtCoordinatorAPIServerSvcPort represents default yurt coordinator apiServer port + DefaultYurtCoordinatorAPIServerSvcPort = "443" YurtHubNamespace = "kube-system" CacheUserAgentsKey = "cache_agents" diff --git a/pkg/yurthub/poolcoordinator/certmanager/certmanager.go b/pkg/yurthub/yurtcoordinator/certmanager/certmanager.go similarity index 89% rename from pkg/yurthub/poolcoordinator/certmanager/certmanager.go rename to pkg/yurthub/yurtcoordinator/certmanager/certmanager.go index 70a81a72844..144f620da60 100644 --- a/pkg/yurthub/poolcoordinator/certmanager/certmanager.go +++ b/pkg/yurthub/yurtcoordinator/certmanager/certmanager.go @@ -32,8 +32,8 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/constants" "github.com/openyurtio/openyurt/pkg/yurthub/util/fs" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/constants" ) type CertFileType int @@ -47,9 +47,9 @@ const ( ) var certFileNames = map[CertFileType]string{ - RootCA: "pool-coordinator-ca.crt", - YurthubClientCert: "pool-coordinator-yurthub-client.crt", - YurthubClientKey: "pool-coordinator-yurthub-client.key", + RootCA: "yurt-coordinator-ca.crt", + YurthubClientCert: "yurt-coordinator-yurthub-client.crt", + YurthubClientKey: "yurt-coordinator-yurthub-client.key", NodeLeaseProxyClientCert: "node-lease-proxy-client.crt", NodeLeaseProxyClientKey: "node-lease-proxy-client.key", } @@ -67,24 +67,24 @@ func NewCertManager(pkiDir, yurtHubNs string, yurtClient kubernetes.Interface, i secretInformerFunc := func(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { tweakListOptions := func(options *metav1.ListOptions) { - options.FieldSelector = fields.Set{"metadata.name": constants.PoolCoordinatorClientSecretName}.String() + options.FieldSelector = fields.Set{"metadata.name": constants.YurtCoordinatorClientSecretName}.String() } return coreinformers.NewFilteredSecretInformer(yurtClient, yurtHubNs, 0, nil, tweakListOptions) } secretInformer := informerFactory.InformerFor(&corev1.Secret{}, secretInformerFunc) secretInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { - klog.V(4).Infof("notify secret add event for %s", constants.PoolCoordinatorClientSecretName) + klog.V(4).Infof("notify secret add event for %s", constants.YurtCoordinatorClientSecretName) secret := obj.(*corev1.Secret) certMgr.updateCerts(secret) }, UpdateFunc: func(_, newObj interface{}) { - klog.V(4).Infof("notify secret update event for %s", constants.PoolCoordinatorClientSecretName) + klog.V(4).Infof("notify secret update event for %s", constants.YurtCoordinatorClientSecretName) secret := newObj.(*corev1.Secret) certMgr.updateCerts(secret) }, DeleteFunc: func(_ interface{}) { - klog.V(4).Infof("notify secret delete event for %s", constants.PoolCoordinatorClientSecretName) + klog.V(4).Infof("notify secret delete event for %s", constants.YurtCoordinatorClientSecretName) certMgr.deleteCerts() }, }) @@ -126,10 +126,10 @@ func (c *CertManager) GetFilePath(t CertFileType) string { func (c *CertManager) updateCerts(secret *corev1.Secret) { ca, caok := secret.Data["ca.crt"] - // pool-coordinator-yurthub-client.crt should appear with pool-coordinator-yurthub-client.key. So we + // yurt-coordinator-yurthub-client.crt should appear with yurt-coordinator-yurthub-client.key. So we // only check the existence once. - coordinatorClientCrt, cook := secret.Data["pool-coordinator-yurthub-client.crt"] - coordinatorClientKey := secret.Data["pool-coordinator-yurthub-client.key"] + coordinatorClientCrt, cook := secret.Data["yurt-coordinator-yurthub-client.crt"] + coordinatorClientKey := secret.Data["yurt-coordinator-yurthub-client.key"] // node-lease-proxy-client.crt should appear with node-lease-proxy-client.key. So we // only check the existence once. @@ -165,7 +165,7 @@ func (c *CertManager) updateCerts(secret *corev1.Secret) { } if cook { - klog.Infof("updating pool-coordinator-yurthub client cert and key") + klog.Infof("updating yurt-coordinator-yurthub client cert and key") if err := c.createOrUpdateFile(c.GetFilePath(YurthubClientKey), coordinatorClientKey); err != nil { klog.Errorf("failed to update coordinator client key, %v", err) } diff --git a/pkg/yurthub/poolcoordinator/certmanager/certmanager_test.go b/pkg/yurthub/yurtcoordinator/certmanager/certmanager_test.go similarity index 90% rename from pkg/yurthub/poolcoordinator/certmanager/certmanager_test.go rename to pkg/yurthub/yurtcoordinator/certmanager/certmanager_test.go index 87bc2afbfb8..a24aaf89381 100644 --- a/pkg/yurthub/poolcoordinator/certmanager/certmanager_test.go +++ b/pkg/yurthub/yurtcoordinator/certmanager/certmanager_test.go @@ -30,13 +30,13 @@ import ( "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/constants" "github.com/openyurtio/openyurt/pkg/yurthub/util" "github.com/openyurtio/openyurt/pkg/yurthub/util/fs" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/constants" ) const ( - testPKIDir = "/tmp/pool-coordinator-pki" + testPKIDir = "/tmp/yurt-coordinator-pki" caByte = `-----BEGIN CERTIFICATE----- MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl cm5ldGVzMB4XDTIyMTIyODAzMzgyM1oXDTMyMTIyNTAzMzgyM1owFTETMBEGA1UE @@ -190,9 +190,9 @@ type expectFile struct { var ( fileStore = fs.FileSystemOperator{} secretGVR = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"} - poolCoordinatorSecret = &corev1.Secret{ + yurtCoordinatorSecret = &corev1.Secret{ ObjectMeta: v1.ObjectMeta{ - Name: constants.PoolCoordinatorClientSecretName, + Name: constants.YurtCoordinatorClientSecretName, Namespace: util.YurtHubNamespace, }, TypeMeta: v1.TypeMeta{ @@ -201,8 +201,8 @@ var ( }, Data: map[string][]byte{ "ca.crt": []byte(caByte), - "pool-coordinator-yurthub-client.crt": []byte(coordinatorCertByte), - "pool-coordinator-yurthub-client.key": []byte(coordinatorKeyByte), + "yurt-coordinator-yurthub-client.crt": []byte(coordinatorCertByte), + "yurt-coordinator-yurthub-client.key": []byte(coordinatorKeyByte), }, } @@ -223,7 +223,7 @@ var ( ) func TestSecretAdd(t *testing.T) { - t.Run("CertManager should not react for secret that is not pool-coordinator-yurthub-certs", func(t *testing.T) { + t.Run("CertManager should not react for secret that is not yurt-coordinator-yurthub-certs", func(t *testing.T) { fakeClient, certMgr, cancel, err := initFakeClientAndCertManager() if err != nil { t.Errorf("failed to initialize, %v", err) @@ -235,7 +235,7 @@ func TestSecretAdd(t *testing.T) { } // Expect to timeout which indicates the CertManager does not save the cert - // that is not pool-coordinator-yurthub-certs. + // that is not yurt-coordinator-yurthub-certs. err = wait.PollImmediate(50*time.Millisecond, 10*time.Second, func() (done bool, err error) { if certMgr.secret != nil { return false, fmt.Errorf("unexpect cert initialization") @@ -251,7 +251,7 @@ func TestSecretAdd(t *testing.T) { }) if err != wait.ErrWaitTimeout { - t.Errorf("CertManager should not react for add event of secret that is not pool-coordinator-yurthub-certs, %v", err) + t.Errorf("CertManager should not react for add event of secret that is not yurt-coordinator-yurthub-certs, %v", err) } if err := fileStore.DeleteDir(testPKIDir); err != nil { @@ -259,33 +259,33 @@ func TestSecretAdd(t *testing.T) { } }) - t.Run("CertManager should react for pool-coordinator-yurthub-certs", func(t *testing.T) { + t.Run("CertManager should react for yurt-coordinator-yurthub-certs", func(t *testing.T) { fakeClient, certMgr, cancel, err := initFakeClientAndCertManager() if err != nil { t.Errorf("failed to initialize, %v", err) } defer cancel() - if err := fakeClient.Tracker().Add(poolCoordinatorSecret); err != nil { - t.Errorf("failed to add secret %s, %v", poolCoordinatorSecret.Name, err) + if err := fakeClient.Tracker().Add(yurtCoordinatorSecret); err != nil { + t.Errorf("failed to add secret %s, %v", yurtCoordinatorSecret.Name, err) } err = wait.PollImmediate(50*time.Millisecond, 10*time.Second, func() (done bool, err error) { - if pass, err := checkSecret(certMgr, poolCoordinatorSecret, []expectFile{ + if pass, err := checkSecret(certMgr, yurtCoordinatorSecret, []expectFile{ { FilePath: certMgr.GetFilePath(RootCA), - Data: poolCoordinatorSecret.Data["ca.crt"], + Data: yurtCoordinatorSecret.Data["ca.crt"], Exists: true, }, { FilePath: certMgr.GetFilePath(YurthubClientCert), - Data: poolCoordinatorSecret.Data["pool-coordinator-yurthub-client.crt"], + Data: yurtCoordinatorSecret.Data["yurt-coordinator-yurthub-client.crt"], Exists: true, }, { FilePath: certMgr.GetFilePath(YurthubClientKey), - Data: poolCoordinatorSecret.Data["pool-coordinator-yurthub-client.key"], + Data: yurtCoordinatorSecret.Data["yurt-coordinator-yurthub-client.key"], Exists: true, }, { @@ -307,7 +307,7 @@ func TestSecretAdd(t *testing.T) { }) if err != nil { - t.Errorf("failed to check poolcoordinator cert, %v", err) + t.Errorf("failed to check yurtcoordinator cert, %v", err) } if err := fileStore.DeleteDir(testPKIDir); err != nil { @@ -324,26 +324,26 @@ func TestSecretUpdate(t *testing.T) { } defer cancel() - if err := fakeClient.Tracker().Add(poolCoordinatorSecret); err != nil { - t.Errorf("failed to add secret %s, %v", poolCoordinatorSecret.Name, err) + if err := fakeClient.Tracker().Add(yurtCoordinatorSecret); err != nil { + t.Errorf("failed to add secret %s, %v", yurtCoordinatorSecret.Name, err) } err = wait.Poll(50*time.Millisecond, 10*time.Second, func() (done bool, err error) { - if pass, err := checkSecret(certMgr, poolCoordinatorSecret, []expectFile{ + if pass, err := checkSecret(certMgr, yurtCoordinatorSecret, []expectFile{ { FilePath: certMgr.GetFilePath(RootCA), - Data: poolCoordinatorSecret.Data["ca.crt"], + Data: yurtCoordinatorSecret.Data["ca.crt"], Exists: true, }, { FilePath: certMgr.GetFilePath(YurthubClientCert), - Data: poolCoordinatorSecret.Data["pool-coordinator-yurthub-client.crt"], + Data: yurtCoordinatorSecret.Data["yurt-coordinator-yurthub-client.crt"], Exists: true, }, { FilePath: certMgr.GetFilePath(YurthubClientKey), - Data: poolCoordinatorSecret.Data["pool-coordinator-yurthub-client.key"], + Data: yurtCoordinatorSecret.Data["yurt-coordinator-yurthub-client.key"], Exists: true, }, { @@ -368,9 +368,9 @@ func TestSecretUpdate(t *testing.T) { } // test updating existing cert and key - newSecret := poolCoordinatorSecret.DeepCopy() - newSecret.Data["pool-coordinator-yurthub-client.key"] = []byte(newKeyByte) - newSecret.Data["pool-coordinator-yurthub-client.crt"] = []byte(newCertByte) + newSecret := yurtCoordinatorSecret.DeepCopy() + newSecret.Data["yurt-coordinator-yurthub-client.key"] = []byte(newKeyByte) + newSecret.Data["yurt-coordinator-yurthub-client.crt"] = []byte(newCertByte) if err := fakeClient.Tracker().Update(secretGVR, newSecret, newSecret.Namespace); err != nil { t.Errorf("failed to update secret, %v", err) } @@ -384,13 +384,13 @@ func TestSecretUpdate(t *testing.T) { }, { FilePath: certMgr.GetFilePath(YurthubClientCert), - Data: newSecret.Data["pool-coordinator-yurthub-client.crt"], + Data: newSecret.Data["yurt-coordinator-yurthub-client.crt"], Exists: true, }, { FilePath: certMgr.GetFilePath(YurthubClientKey), - Data: newSecret.Data["pool-coordinator-yurthub-client.key"], + Data: newSecret.Data["yurt-coordinator-yurthub-client.key"], Exists: true, }, { @@ -430,13 +430,13 @@ func TestSecretUpdate(t *testing.T) { }, { FilePath: certMgr.GetFilePath(YurthubClientCert), - Data: newSecret.Data["pool-coordinator-yurthub-client.crt"], + Data: newSecret.Data["yurt-coordinator-yurthub-client.crt"], Exists: true, }, { FilePath: certMgr.GetFilePath(YurthubClientKey), - Data: newSecret.Data["pool-coordinator-yurthub-client.key"], + Data: newSecret.Data["yurt-coordinator-yurthub-client.key"], Exists: true, }, { @@ -479,26 +479,26 @@ func TestSecretDelete(t *testing.T) { } defer cancel() - if err := fakeClient.Tracker().Add(poolCoordinatorSecret); err != nil { - t.Errorf("failed to add secret %s, %v", poolCoordinatorSecret.Name, err) + if err := fakeClient.Tracker().Add(yurtCoordinatorSecret); err != nil { + t.Errorf("failed to add secret %s, %v", yurtCoordinatorSecret.Name, err) } err = wait.PollImmediate(50*time.Millisecond, 10*time.Second, func() (done bool, err error) { - if pass, err := checkSecret(certMgr, poolCoordinatorSecret, []expectFile{ + if pass, err := checkSecret(certMgr, yurtCoordinatorSecret, []expectFile{ { FilePath: certMgr.GetFilePath(RootCA), - Data: poolCoordinatorSecret.Data["ca.crt"], + Data: yurtCoordinatorSecret.Data["ca.crt"], Exists: true, }, { FilePath: certMgr.GetFilePath(YurthubClientCert), - Data: poolCoordinatorSecret.Data["pool-coordinator-yurthub-client.crt"], + Data: yurtCoordinatorSecret.Data["yurt-coordinator-yurthub-client.crt"], Exists: true, }, { FilePath: certMgr.GetFilePath(YurthubClientKey), - Data: poolCoordinatorSecret.Data["pool-coordinator-yurthub-client.key"], + Data: yurtCoordinatorSecret.Data["yurt-coordinator-yurthub-client.key"], Exists: true, }, { @@ -522,7 +522,7 @@ func TestSecretDelete(t *testing.T) { t.Errorf("failed to wait cert manager to be initialized, %v", err) } - if err := fakeClient.Tracker().Delete(secretGVR, poolCoordinatorSecret.Namespace, poolCoordinatorSecret.Name); err != nil { + if err := fakeClient.Tracker().Delete(secretGVR, yurtCoordinatorSecret.Namespace, yurtCoordinatorSecret.Name); err != nil { t.Errorf("failed to delete secret, %v", err) } diff --git a/pkg/yurthub/poolcoordinator/constants/constants.go b/pkg/yurthub/yurtcoordinator/constants/constants.go similarity index 93% rename from pkg/yurthub/poolcoordinator/constants/constants.go rename to pkg/yurthub/yurtcoordinator/constants/constants.go index 9f25267b215..50e2541c6c9 100644 --- a/pkg/yurthub/poolcoordinator/constants/constants.go +++ b/pkg/yurthub/yurtcoordinator/constants/constants.go @@ -29,5 +29,5 @@ var ( const ( DefaultPoolScopedUserAgent = "leader-yurthub" - PoolCoordinatorClientSecretName = "pool-coordinator-yurthub-certs" + YurtCoordinatorClientSecretName = "yurt-coordinator-yurthub-certs" ) diff --git a/pkg/yurthub/poolcoordinator/coordinator.go b/pkg/yurthub/yurtcoordinator/coordinator.go similarity index 93% rename from pkg/yurthub/poolcoordinator/coordinator.go rename to pkg/yurthub/yurtcoordinator/coordinator.go index 194f9933260..707c0f5bcc6 100644 --- a/pkg/yurthub/poolcoordinator/coordinator.go +++ b/pkg/yurthub/yurtcoordinator/coordinator.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinator +package yurtcoordinator import ( "context" @@ -48,13 +48,13 @@ import ( yurtrest "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/rest" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer" "github.com/openyurtio/openyurt/pkg/yurthub/metrics" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/certmanager" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/constants" - "github.com/openyurtio/openyurt/pkg/yurthub/poolcoordinator/resources" "github.com/openyurtio/openyurt/pkg/yurthub/storage" "github.com/openyurtio/openyurt/pkg/yurthub/storage/disk" "github.com/openyurtio/openyurt/pkg/yurthub/storage/etcd" "github.com/openyurtio/openyurt/pkg/yurthub/transport" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/certmanager" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/constants" + "github.com/openyurtio/openyurt/pkg/yurthub/yurtcoordinator/resources" ) const ( @@ -65,19 +65,19 @@ const ( nameInformerLease = "leader-informer-sync" ) -// Coordinator will track the status of pool coordinator, and change the +// Coordinator will track the status of yurt coordinator, and change the // cache and proxy behaviour of yurthub accordingly. type Coordinator interface { // Start the Coordinator. Run() - // IsReady will return the poolCacheManager and true if the pool-coordinator is ready. - // Pool-Coordinator ready means it is ready to handle request. To be specific, it should + // IsReady will return the poolCacheManager and true if the yurt-coordinator is ready. + // Yurt-Coordinator ready means it is ready to handle request. To be specific, it should // satisfy the following 3 condition: - // 1. Pool-Coordinator is healthy + // 1. Yurt-Coordinator is healthy // 2. Pool-Scoped resources have been synced with cloud, through list/watch - // 3. local cache has been uploaded to pool-coordinator + // 3. local cache has been uploaded to yurt-coordinator IsReady() (cachemanager.CacheManager, bool) - // IsCoordinatorHealthy will return the poolCacheManager and true if the pool-coordinator is healthy. + // IsCoordinatorHealthy will return the poolCacheManager and true if the yurt-coordinator is healthy. // We assume coordinator is healthy when the elect status is LeaderHub and FollowerHub. IsHealthy() (cachemanager.CacheManager, bool) } @@ -111,14 +111,14 @@ type coordinator struct { // pick a healthy cloud APIServer to proxy heartbeats. cloudHealthChecker healthchecker.MultipleBackendsHealthChecker needUploadLocalCache bool - // poolCacheSyncManager is used to sync pool-scoped resources from cloud to poolcoordinator. + // poolCacheSyncManager is used to sync pool-scoped resources from cloud to yurtcoordinator. poolCacheSyncManager *poolScopedCacheSyncManager // poolCacheSyncedDector is used to detect if pool cache is synced and ready for use. // It will list/watch the informer sync lease, and if it's renewed by leader yurthub, isPoolCacheSynced will // be set as true which means the pool cache is ready for use. It also starts a routine which will set // isPoolCacheSynced as false if the informer sync lease has not been updated for a duration. poolCacheSyncedDetector *poolCacheSyncedDetector - // delegateNodeLeaseManager is used to list/watch kube-node-lease from poolcoordinator. If the + // delegateNodeLeaseManager is used to list/watch kube-node-lease from yurtcoordinator. If the // node lease contains DelegateHeartBeat label, it will triger the eventhandler which will // use cloud client to send it to cloud APIServer. delegateNodeLeaseManager *coordinatorLeaseInformerManager @@ -148,7 +148,7 @@ func NewCoordinator( } coordinatorClient, err := kubernetes.NewForConfig(coordinatorRESTCfg) if err != nil { - return nil, fmt.Errorf("failed to create client for pool coordinator, %v", err) + return nil, fmt.Errorf("failed to create client for yurt coordinator, %v", err) } coordinator := &coordinator{ @@ -238,7 +238,7 @@ func (coordinator *coordinator) Run() { if !ok { return } - metrics.Metrics.ObservePoolCoordinatorYurthubRole(electorStatus) + metrics.Metrics.ObserveYurtCoordinatorYurthubRole(electorStatus) if coordinator.cnt == math.MaxUint64 { // cnt will overflow, reset it. @@ -364,34 +364,34 @@ func (coordinator *coordinator) Run() { } } -// IsReady will return the poolCacheManager and true if the pool-coordinator is ready. -// Pool-Coordinator ready means it is ready to handle request. To be specific, it should +// IsReady will return the poolCacheManager and true if the yurt-coordinator is ready. +// Yurt-Coordinator ready means it is ready to handle request. To be specific, it should // satisfy the following 3 condition: -// 1. Pool-Coordinator is healthy +// 1. Yurt-Coordinator is healthy // 2. Pool-Scoped resources have been synced with cloud, through list/watch -// 3. local cache has been uploaded to pool-coordinator +// 3. local cache has been uploaded to yurt-coordinator func (coordinator *coordinator) IsReady() (cachemanager.CacheManager, bool) { - // If electStatus is not PendingHub, it means pool-coordinator is healthy. + // If electStatus is not PendingHub, it means yurt-coordinator is healthy. coordinator.Lock() defer coordinator.Unlock() if coordinator.electStatus != PendingHub && coordinator.isPoolCacheSynced && !coordinator.needUploadLocalCache { - metrics.Metrics.ObservePoolCoordinatorReadyStatus(1) + metrics.Metrics.ObserveYurtCoordinatorReadyStatus(1) return coordinator.poolCacheManager, true } - metrics.Metrics.ObservePoolCoordinatorReadyStatus(0) + metrics.Metrics.ObserveYurtCoordinatorReadyStatus(0) return nil, false } -// IsCoordinatorHealthy will return the poolCacheManager and true if the pool-coordinator is healthy. +// IsCoordinatorHealthy will return the poolCacheManager and true if the yurt-coordinator is healthy. // We assume coordinator is healthy when the elect status is LeaderHub and FollowerHub. func (coordinator *coordinator) IsHealthy() (cachemanager.CacheManager, bool) { coordinator.Lock() defer coordinator.Unlock() if coordinator.electStatus != PendingHub { - metrics.Metrics.ObservePoolCoordinatorHealthyStatus(1) + metrics.Metrics.ObserveYurtCoordinatorHealthyStatus(1) return coordinator.poolCacheManager, true } - metrics.Metrics.ObservePoolCoordinatorHealthyStatus(0) + metrics.Metrics.ObserveYurtCoordinatorHealthyStatus(0) return nil, false } @@ -452,9 +452,9 @@ func (coordinator *coordinator) uploadLocalCache(etcdStore storage.Store) error func (coordinator *coordinator) delegateNodeLease(cloudLeaseClient coordclientset.LeaseInterface, obj interface{}) { newLease := obj.(*coordinationv1.Lease) for i := 0; i < leaseDelegateRetryTimes; i++ { - // ResourceVersions of lease objects in pool-coordinator always have different rv + // ResourceVersions of lease objects in yurt-coordinator always have different rv // from what of cloud lease. So we should get cloud lease first and then update - // it with lease from pool-coordinator. + // it with lease from yurt-coordinator. cloudLease, err := cloudLeaseClient.Get(coordinator.ctx, newLease.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { if _, err := cloudLeaseClient.Create(coordinator.ctx, cloudLease, metav1.CreateOptions{}); err != nil { @@ -475,17 +475,17 @@ func (coordinator *coordinator) delegateNodeLease(cloudLeaseClient coordclientse } } -// poolScopedCacheSyncManager will continuously sync pool-scoped resources from cloud to pool-coordinator. +// poolScopedCacheSyncManager will continuously sync pool-scoped resources from cloud to yurt-coordinator. // After resource sync is completed, it will periodically renew the informer synced lease, which is used by -// other yurthub to determine if pool-coordinator is ready to handle requests of pool-scoped resources. +// other yurthub to determine if yurt-coordinator is ready to handle requests of pool-scoped resources. // It uses proxied client to list/watch pool-scoped resources from cloud APIServer, which -// will be automatically cached into pool-coordinator through YurtProxyServer. +// will be automatically cached into yurt-coordinator through YurtProxyServer. type poolScopedCacheSyncManager struct { ctx context.Context isRunning bool // dynamicClient is a dynamic client of Cloud APIServer which is proxied by yurthub. dynamicClient dynamic.Interface - // coordinatorClient is a client of APIServer in pool-coordinator. + // coordinatorClient is a client of APIServer in yurt-coordinator. coordinatorClient kubernetes.Interface // nodeName will be used to update the ownerReference of informer synced lease. nodeName string @@ -567,10 +567,10 @@ func (p *poolScopedCacheSyncManager) renewInformerLease(ctx context.Context, lea } } -// coordinatorLeaseInformerManager will use pool-coordinator client to list/watch -// lease in pool-coordinator. Through passing different event handler, it can either +// coordinatorLeaseInformerManager will use yurt-coordinator client to list/watch +// lease in yurt-coordinator. Through passing different event handler, it can either // delegating node lease by leader yurthub or detecting the informer synced lease to -// check if pool-coordinator is ready for requests of pool-scoped resources. +// check if yurt-coordinator is ready for requests of pool-scoped resources. type coordinatorLeaseInformerManager struct { ctx context.Context coordinatorClient kubernetes.Interface @@ -602,7 +602,7 @@ func (c *coordinatorLeaseInformerManager) EnsureStop() { } // localCacheUploader can upload resources in local cache to pool cache. -// Currently, we only upload pods and nodes to pool-coordinator. +// Currently, we only upload pods and nodes to yurt-coordinator. type localCacheUploader struct { diskStorage storage.Store etcdStorage storage.Store diff --git a/pkg/yurthub/poolcoordinator/coordinator_test.go b/pkg/yurthub/yurtcoordinator/coordinator_test.go similarity index 99% rename from pkg/yurthub/poolcoordinator/coordinator_test.go rename to pkg/yurthub/yurtcoordinator/coordinator_test.go index 6ba49afd4eb..eb284ed7e26 100644 --- a/pkg/yurthub/poolcoordinator/coordinator_test.go +++ b/pkg/yurthub/yurtcoordinator/coordinator_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinator +package yurtcoordinator import ( "context" diff --git a/pkg/yurthub/poolcoordinator/fake_coordinator.go b/pkg/yurthub/yurtcoordinator/fake_coordinator.go similarity index 97% rename from pkg/yurthub/poolcoordinator/fake_coordinator.go rename to pkg/yurthub/yurtcoordinator/fake_coordinator.go index 9b8ea520a7a..a5b444762bc 100644 --- a/pkg/yurthub/poolcoordinator/fake_coordinator.go +++ b/pkg/yurthub/yurtcoordinator/fake_coordinator.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinator +package yurtcoordinator import "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" diff --git a/pkg/yurthub/poolcoordinator/informer_lease.go b/pkg/yurthub/yurtcoordinator/informer_lease.go similarity index 99% rename from pkg/yurthub/poolcoordinator/informer_lease.go rename to pkg/yurthub/yurtcoordinator/informer_lease.go index b48a9ff5f59..bc42e6064f8 100644 --- a/pkg/yurthub/poolcoordinator/informer_lease.go +++ b/pkg/yurthub/yurtcoordinator/informer_lease.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinator +package yurtcoordinator import ( "context" diff --git a/pkg/yurthub/poolcoordinator/leader_election.go b/pkg/yurthub/yurtcoordinator/leader_election.go similarity index 99% rename from pkg/yurthub/poolcoordinator/leader_election.go rename to pkg/yurthub/yurtcoordinator/leader_election.go index c8e3003f325..3d30fcee891 100644 --- a/pkg/yurthub/poolcoordinator/leader_election.go +++ b/pkg/yurthub/yurtcoordinator/leader_election.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinator +package yurtcoordinator import ( "context" diff --git a/pkg/yurthub/poolcoordinator/resources/resources.go b/pkg/yurthub/yurtcoordinator/resources/resources.go similarity index 100% rename from pkg/yurthub/poolcoordinator/resources/resources.go rename to pkg/yurthub/yurtcoordinator/resources/resources.go diff --git a/pkg/yurthub/poolcoordinator/resources/verifiable_pool_scope_resource.go b/pkg/yurthub/yurtcoordinator/resources/verifiable_pool_scope_resource.go similarity index 100% rename from pkg/yurthub/poolcoordinator/resources/verifiable_pool_scope_resource.go rename to pkg/yurthub/yurtcoordinator/resources/verifiable_pool_scope_resource.go From dede3c989160d5c7bbb81d13c589c1b24c02cc47 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:47:10 +0800 Subject: [PATCH 25/93] rename pool-coordinator to yurt-coordinator for controller (#1531) --- pkg/controller/controller.go | 8 +- .../csrapprover/csrapprover_controller.go | 12 +- .../cert/certificate.go | 24 +-- .../cert/certificate_test.go | 34 ++-- .../cert/secret.go | 2 +- .../cert/secret_test.go | 2 +- .../cert/util.go | 12 +- .../cert/util_test.go | 12 +- .../cert/yurtcoordinatorcert_controller.go} | 184 +++++++++--------- .../yurtcoordinatorcert_controller_test.go} | 10 +- .../constant/constant.go | 0 .../delegatelease/delegatelease_controller.go | 4 +- .../delegatelease_controller_test.go | 2 +- .../podbinding/podbinding_controller.go | 2 +- .../utils/lease.go | 2 +- .../utils/lease_test.go | 0 .../utils/taints.go | 0 .../utils/taints_test.go | 2 +- pkg/webhook/pod/v1/pod_validation.go | 2 +- 19 files changed, 157 insertions(+), 157 deletions(-) rename pkg/controller/{poolcoordinator => yurtcoordinator}/cert/certificate.go (94%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/cert/certificate_test.go (95%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/cert/secret.go (99%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/cert/secret_test.go (98%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/cert/util.go (83%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/cert/util_test.go (92%) rename pkg/controller/{poolcoordinator/cert/poolcoordinatorcert_controller.go => yurtcoordinator/cert/yurtcoordinatorcert_controller.go} (69%) rename pkg/controller/{poolcoordinator/cert/poolcoordinatorcert_controller_test.go => yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go} (91%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/constant/constant.go (100%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/delegatelease/delegatelease_controller.go (98%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/delegatelease/delegatelease_controller_test.go (94%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/podbinding/podbinding_controller.go (99%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/utils/lease.go (95%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/utils/lease_test.go (100%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/utils/taints.go (100%) rename pkg/controller/{poolcoordinator => yurtcoordinator}/utils/taints_test.go (95%) diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index abec0324a27..334197a1322 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -25,9 +25,6 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/csrapprover" "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater" "github.com/openyurtio/openyurt/pkg/controller/nodepool" - poolcoordinatorcert "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/cert" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/delegatelease" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/podbinding" "github.com/openyurtio/openyurt/pkg/controller/raven" "github.com/openyurtio/openyurt/pkg/controller/raven/gateway" "github.com/openyurtio/openyurt/pkg/controller/raven/service" @@ -37,6 +34,9 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/util" "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon" "github.com/openyurtio/openyurt/pkg/controller/yurtappset" + yurtcoordinatorcert "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/cert" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/delegatelease" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/podbinding" "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset" ) @@ -57,7 +57,7 @@ func init() { controllerAddFuncs[podbinding.ControllerName] = []AddControllerFn{podbinding.Add} controllerAddFuncs[raven.ControllerName] = []AddControllerFn{gateway.Add, service.Add} controllerAddFuncs[nodepool.ControllerName] = []AddControllerFn{nodepool.Add} - controllerAddFuncs[poolcoordinatorcert.ControllerName] = []AddControllerFn{poolcoordinatorcert.Add} + controllerAddFuncs[yurtcoordinatorcert.ControllerName] = []AddControllerFn{yurtcoordinatorcert.Add} controllerAddFuncs[servicetopology.ControllerName] = []AddControllerFn{servicetopologyendpoints.Add, servicetopologyendpointslice.Add} controllerAddFuncs[yurtstaticset.ControllerName] = []AddControllerFn{yurtstaticset.Add} controllerAddFuncs[yurtappset.ControllerName] = []AddControllerFn{yurtappset.Add} diff --git a/pkg/controller/csrapprover/csrapprover_controller.go b/pkg/controller/csrapprover/csrapprover_controller.go index f091f513936..ae4a65497a8 100644 --- a/pkg/controller/csrapprover/csrapprover_controller.go +++ b/pkg/controller/csrapprover/csrapprover_controller.go @@ -43,7 +43,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - poolcoordinatorCert "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/cert" + yurtcoorrdinatorCert "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/cert" "github.com/openyurtio/openyurt/pkg/projectinfo" "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token" "github.com/openyurtio/openyurt/pkg/yurttunnel/constants" @@ -87,8 +87,8 @@ var ( successMsg: "Auto approving tunnel-agent client certificate", }, { - recognize: isPoolCoordinatorClientCert, - successMsg: "Auto approving poolcoordinator-apiserver client certificate", + recognize: isYurtCoordinatorClientCert, + successMsg: "Auto approving yurtcoordinator-apiserver client certificate", }, } ) @@ -434,13 +434,13 @@ func isYurtTunnelAgentCert(csr *certificatesv1.CertificateSigningRequest, x509cr return true } -// isYurtTunnelProxyClientCert is used to recognize csr from poolcoordinator client certificate . -func isPoolCoordinatorClientCert(csr *certificatesv1.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool { +// isYurtTunnelProxyClientCert is used to recognize csr from yurtcoordinator client certificate . +func isYurtCoordinatorClientCert(csr *certificatesv1.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool { if csr.Spec.SignerName != certificatesv1.KubeAPIServerClientSignerName { return false } - if len(x509cr.Subject.Organization) != 1 || x509cr.Subject.Organization[0] != poolcoordinatorCert.PoolcoordinatorOrg { + if len(x509cr.Subject.Organization) != 1 || x509cr.Subject.Organization[0] != yurtcoorrdinatorCert.YurtCoordinatorOrg { return false } diff --git a/pkg/controller/poolcoordinator/cert/certificate.go b/pkg/controller/yurtcoordinator/cert/certificate.go similarity index 94% rename from pkg/controller/poolcoordinator/cert/certificate.go rename to pkg/controller/yurtcoordinator/cert/certificate.go index eb3fdad2a40..7c5bf2d68b9 100644 --- a/pkg/controller/poolcoordinator/cert/certificate.go +++ b/pkg/controller/yurtcoordinator/cert/certificate.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinatorcert +package yurtcoordinatorcert import ( "context" @@ -64,7 +64,7 @@ func NewSelfSignedCA() (*x509.Certificate, crypto.Signer, error) { } cert, err := certutil.NewSelfSignedCACert(certutil.Config{ - CommonName: PoolcoordinatorOrg, + CommonName: YurtCoordinatorOrg, }, key) if err != nil { return nil, nil, errors.Wrap(err, "Create CA cert fail") @@ -134,7 +134,7 @@ func loadCertAndKeyFromSecret(clientSet client.Interface, certConf CertConfig) ( certName := certConf.CertName // get secret - secret, err := clientSet.CoreV1().Secrets(PoolcoordinatorNS).Get(context.TODO(), secretName, metav1.GetOptions{}) + secret, err := clientSet.CoreV1().Secrets(YurtCoordinatorNS).Get(context.TODO(), secretName, metav1.GetOptions{}) if err != nil { return nil, nil, err } @@ -209,26 +209,26 @@ func IsCertFromCA(cert *x509.Certificate, caCert *x509.Certificate) bool { return true } -func initPoolCoordinatorCert(client client.Interface, cfg CertConfig, caCert *x509.Certificate, caKey crypto.Signer, stopCh <-chan struct{}) error { +func initYurtCoordinatorCert(client client.Interface, cfg CertConfig, caCert *x509.Certificate, caKey crypto.Signer, stopCh <-chan struct{}) error { key, err := NewPrivateKey() if err != nil { - return errors.Wrapf(err, "init poolcoordinator cert: create %s key fail", cfg.CertName) + return errors.Wrapf(err, "init yurtcoordinator cert: create %s key fail", cfg.CertName) } cert, err := NewSignedCert(client, &cfg, key, caCert, caKey, stopCh) if err != nil { - return errors.Wrapf(err, "init poolcoordinator cert: create %s cert fail", cfg.CertName) + return errors.Wrapf(err, "init yurtcoordinator cert: create %s cert fail", cfg.CertName) } if !cfg.IsKubeConfig { err = WriteCertAndKeyIntoSecret(client, cfg.CertName, cfg.SecretName, cert, key) if err != nil { - return errors.Wrapf(err, "init poolcoordinator cert: write %s into secret %s fail", cfg.CertName, cfg.SecretName) + return errors.Wrapf(err, "init yurtcoordinator cert: write %s into secret %s fail", cfg.CertName, cfg.SecretName) } } else { apiServerURL, err := getAPIServerSVCURL(client) if err != nil { - return errors.Wrapf(err, "couldn't get PoolCoordinator APIServer service url") + return errors.Wrapf(err, "couldn't get YurtCoordinator APIServer service url") } keyBytes, _ := keyutil.MarshalPrivateKeyToPEM(key) @@ -338,7 +338,7 @@ func WriteCertIntoSecret(clientSet client.Interface, certName, secretName string } // write certificate data into secret - secretClient, err := NewSecretClient(clientSet, PoolcoordinatorNS, secretName) + secretClient, err := NewSecretClient(clientSet, YurtCoordinatorNS, secretName) if err != nil { return err } @@ -360,7 +360,7 @@ func WriteCertIntoSecret(clientSet client.Interface, certName, secretName string // Notice: if cert OR key is nil, it will be ignored func WriteCertAndKeyIntoSecret(clientSet client.Interface, certName, secretName string, cert *x509.Certificate, key crypto.Signer) error { // write certificate data into secret - secretClient, err := NewSecretClient(clientSet, PoolcoordinatorNS, secretName) + secretClient, err := NewSecretClient(clientSet, YurtCoordinatorNS, secretName) if err != nil { return err } @@ -393,7 +393,7 @@ func WriteCertAndKeyIntoSecret(clientSet client.Interface, certName, secretName } func WriteKubeConfigIntoSecret(clientSet client.Interface, secretName, kubeConfigName string, kubeConfigByte []byte) error { - secretClient, err := NewSecretClient(clientSet, PoolcoordinatorNS, secretName) + secretClient, err := NewSecretClient(clientSet, YurtCoordinatorNS, secretName) if err != nil { return err } @@ -408,7 +408,7 @@ func WriteKubeConfigIntoSecret(clientSet client.Interface, secretName, kubeConfi } func WriteKeyPairIntoSecret(clientSet client.Interface, secretName, keyName string, key crypto.Signer) error { - secretClient, err := NewSecretClient(clientSet, PoolcoordinatorNS, secretName) + secretClient, err := NewSecretClient(clientSet, YurtCoordinatorNS, secretName) if err != nil { return err } diff --git a/pkg/controller/poolcoordinator/cert/certificate_test.go b/pkg/controller/yurtcoordinator/cert/certificate_test.go similarity index 95% rename from pkg/controller/poolcoordinator/cert/certificate_test.go rename to pkg/controller/yurtcoordinator/cert/certificate_test.go index 546111d45b6..4d66f9633d6 100644 --- a/pkg/controller/poolcoordinator/cert/certificate_test.go +++ b/pkg/controller/yurtcoordinator/cert/certificate_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinatorcert +package yurtcoordinatorcert import ( "crypto/x509" @@ -228,12 +228,12 @@ clusters: contexts: - context: cluster: cluster - user: openyurt:pool-coordinator:monitoring - name: openyurt:pool-coordinator:monitoring@cluster -current-context: openyurt:pool-coordinator:monitoring@cluster + user: openyurt:yurt-coordinator:monitoring + name: openyurt:yurt-coordinator:monitoring@cluster +current-context: openyurt:yurt-coordinator:monitoring@cluster kind: Config users: -- name: openyurt:pool-coordinator:monitoring +- name: openyurt:yurt-coordinator:monitoring user: client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVCVENDQXUyZ0F3SUJBZ0lJQ2tZTjVhSGc1d293RFFZSktvWklodmNOQVFFTEJRQXdKREVpTUNBR0ExVUUKQXhNWmIzQmxibmwxY25RNmNHOXZiQzFqYjI5eVpHbHVZWFJ2Y2pBZ0Z3MHlNakV5TWpreE16VTVNVE5hR0E4eQpNVEl5TVRJd05URTNNalUwT1Zvd1V6RWlNQ0FHQTFVRUNoTVpiM0JsYm5sMWNuUTZjRzl2YkMxamIyOXlaR2x1CllYUnZjakV0TUNzR0ExVUVBeE1rYjNCbGJubDFjblE2Y0c5dmJDMWpiMjl5WkdsdVlYUnZjanB0YjI1cGRHOXkKYVc1bk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGhVQjEwWlVidzVzYXduWgo5L1I2TnpuMk9MYmZyS2E1ckxkcWNaNXNHR3ZUOUQ3TFRnUUV1QlJVRkEzRXFwRUFoZmppNXcxOGhZdVNVdUR2CmNJOCtRZWJCZDNpMEtGV1hSNmpJMWZlbUhnRFpKQXplK0VKOVRycnZqNjMzd1g2N3l3UnRiWUNjaFY5VUxLbnYKNkdNMWh4aW1XL25IZHBBN1hRdnVFU1pMbGRkcDNZbXRNYk5kZUV0Y1pUVldwZlhQSFI1VWRiZENncTJybENoNgpuNUdKbzA4TGsvdVR5dngvSlllcVZFZ0ErK1FIeFJuZWZ4ZHVqNlBWbVNJa1M4Uk1haUUwL05KVUM3NlZPTDhtClV5a2NvUmV6SHkzSVNGUFBQYTJVWVJueUsvWHBMTjhWYVlISldFcUlLL1hMZjloaGVjZldxem44OEFTRkNybVkKVFJSVEVRSURBUUFCbzRJQkNEQ0NBUVF3RGdZRFZSMFBBUUgvQkFRREFnV2dNQk1HQTFVZEpRUU1NQW9HQ0NzRwpBUVVGQndNQ01COEdBMVVkSXdRWU1CYUFGRmFwTWdPay9KRmFFNUd3QTgyYjhLYjI2RWVmTUlHN0JnTlZIUkVFCmdiTXdnYkNDR25CdmIyd3RZMjl2Y21ScGJtRjBiM0l0WVhCcGMyVnlkbVZ5Z2lad2IyOXNMV052YjNKa2FXNWgKZEc5eUxXRndhWE5sY25abGNpNXJkV0psTFhONWMzUmxiWUlxY0c5dmJDMWpiMjl5WkdsdVlYUnZjaTFoY0dsegpaWEoyWlhJdWEzVmlaUzF6ZVhOMFpXMHVjM1pqZ2pod2IyOXNMV052YjNKa2FXNWhkRzl5TFdGd2FYTmxjblpsCmNpNXJkV0psTFhONWMzUmxiUzV6ZG1NdVkyeDFjM1JsY2k1c2IyTmhiSWNFQ21DcnF6QU5CZ2txaGtpRzl3MEIKQVFzRkFBT0NBUUVBZzVRVVVyeEtWakp1by9GcFJCcmFzcGtJTk9ZUmNNV2dzaWpwMUoxbWxZdkExWWMweitVQgp2cmQ4aFJsa2pFdGQwYzl0UjZPYVE5M1hPQU8zK1laVkZRcUN3eU5nM2g1c3dGQzhuQ3VhclRHZ2kxcDRxVTRRCm9XbmRUdTJqeDVmcUowazVzdHlieW0rY2ZnTkpsM3JyY2pBem1PRmEvbUFMSDFYVFYwZGIyZFpBai9WV01iK0IKSFlmc3lyb2daVnpnOXJVZTNEME1KZFcwc3BxbXZFYlVsWkhHLzFteFVvQStvdzhoVDhhdmUyenFSZ3lNTEhHTwo2NFk4aXY3d003N1N2dWtyOWdkVFRWQXhVRkhMcDBtazU4K1ZoSU9sRldyVnBpc3A4TlVCZHIrT2lzdXhyZ0FWCkJZTlh6eDZCb3ZBLzh4SDdVZlh6OFVic0gwc2lTdGRyN0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBOGhVQjEwWlVidzVzYXduWjkvUjZOem4yT0xiZnJLYTVyTGRxY1o1c0dHdlQ5RDdMClRnUUV1QlJVRkEzRXFwRUFoZmppNXcxOGhZdVNVdUR2Y0k4K1FlYkJkM2kwS0ZXWFI2akkxZmVtSGdEWkpBemUKK0VKOVRycnZqNjMzd1g2N3l3UnRiWUNjaFY5VUxLbnY2R00xaHhpbVcvbkhkcEE3WFF2dUVTWkxsZGRwM1ltdApNYk5kZUV0Y1pUVldwZlhQSFI1VWRiZENncTJybENoNm41R0pvMDhMay91VHl2eC9KWWVxVkVnQSsrUUh4Um5lCmZ4ZHVqNlBWbVNJa1M4Uk1haUUwL05KVUM3NlZPTDhtVXlrY29SZXpIeTNJU0ZQUFBhMlVZUm55Sy9YcExOOFYKYVlISldFcUlLL1hMZjloaGVjZldxem44OEFTRkNybVlUUlJURVFJREFRQUJBb0lCQUROYzA2d3FSdVhkU0pHWgpZSDdraHozS2RYeHBDS0lvS2NNRWszZ1I1ZHQwblY3NEo4aWd2Nk9TNUpmd3ArYU1wM0RGY3RjVkhITjFQcEdKCkdpUm1zQTNwZU9qeFdrQW9rTlZxY1ZvOGxpbE5nc1RNV2s2UVJPZjhiN0dyZHFLK1VmZnNNNCtGTnpCeEhubnYKZ0hCdEJFRnFzSGxaVU1IT0xsbzZtc05XdmJqSHR4QVRqS1JQN2pGa0w1Z0pCelQxeFQ3a0dnTlhkNjNZc1c4MAp4MFEyc0pTRW1qTSt2ZEcvMkZhV2V3RytZRmY1c0lXbGFudFZlaDJUODZjYWNlWm1OeVV2bVc5WFVUNGF6TExXCjllZ0pRVjBYNjJkRjJTRGlIUDc5OGFvY0lzSk1xSjYzWGx0b1IzWlJkYVhUMVhuT05uc0U4OWU2WlhlcUtMeTIKN1dxQVZVRUNnWUVBKy9mMUFTQ0E2a2dBNzVpRTdKc1g4c3ZSSzB6VFR6QWhJMkdnWCtmbDgzR3B5WGpNMVl2NgoxOFNobVBKeXdaTHRiMkdDOUxPMlVtdTNKeiszcTVvbHRkZStieCsyMWRGUVZycUZBWHFRM01qSFZ1NDhOQXI4CkFpN1lnTkx6WkF1MHZhZG5FbFpCK0NOVzI2Q0dSbUgzRGQ0bGptWjRvRHdEaFU2dmhiQ1diZGtDZ1lFQTlmU08KUlRwVmxpbTJEY3ZzUG82RmhjWi9ScnpLN2hhS3RFWTZKUjZmbGpWaEtaWUUvSk5WV05TckRRUEVGbDNBSlZXdwp5OWhONnZJeU5ydjhGTWc1d3U4Zi9HOFhCdWF1Q21YSXVIc29ZUU80a3RsTjYySFVFemdPQzVVZERzV0U3RE5ECjlybk1kV0hJZ0cxdmtkT2l3dy9NRHYrNnV3NTdEZHVaOHhEamMva0NnWUFVdisyd1F4SDZ1U1ZDbGVmVWFFMUgKbEZ0TVdvNUlSaWxrZFlTMGdTOWhwZW1haXRVcmZOU1Nja0h3aTM3QnpDeTdjR2ROYVlOSk5FK243c3BjV2x4aQpwanFyZ2d3WGZaNUZGaVVmNHcwTThZZmc4OHVIYWFRcE5keGtkM3JOc1YwWUJUSXF3Mm01V29lcm5JT1NSajBICktsVWpiZkxmRnpJZkIwVFRHS0M2dVFLQmdEVlBiYXJwcXZWaVV4aUljOHRYWHUrUkI3TlFabmZXb1BmVUpQUTQKd0FSeHkzNlZDcjJvUFo2RWNoTGZGeGgxOTVqZ0N2TVVEa2QzZVpUTmlDVUZCU2dRWnBGempyMHJNTndHRmN5Twp2VURSNnFiQnZSYmczSFBSK1pGZkg2NDg5OE91bFBPY2NBbWRTVFUxQXpMTGVZTG9JS1c3bmtDL01jTGVMMjgwCjRPZ1pBb0dCQU12SUNaK3drbVdZRXZMK0VTWE1MTTdvb2tSdDMyL1piTXUxNmY2U01JRnhuOGtIYTBINEZzamoKQVkzeWFBYlBwVGs5L3Z0c1l1U2JYVEpqRER6VmtFTk1CaDdrN0JTUGthalU1SXk0ODdpY0hnekFKSzl5WGFvcAprZnhKT0FFVyt5Y2lsazFmbnREQVhibHFNQTVxYm5JR0VCNk9ZUlFIMW5HaEJrdm5YYllqCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==` @@ -241,14 +241,14 @@ users: emptySecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test", - Namespace: PoolcoordinatorNS, + Namespace: YurtCoordinatorNS, }, Data: make(map[string][]byte), } kubeConfigSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test", - Namespace: PoolcoordinatorNS, + Namespace: YurtCoordinatorNS, }, Data: map[string][]byte{ "kubeconfig": []byte(kubeConfigStr), @@ -257,7 +257,7 @@ users: certSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test", - Namespace: PoolcoordinatorNS, + Namespace: YurtCoordinatorNS, }, Data: map[string][]byte{ "test.crt": []byte(cert), @@ -299,7 +299,7 @@ users: IsKubeConfig: true, CertName: "kubeconfig", SecretName: "test", - CommonName: "openyurt:pool-coordinator:monitoring", + CommonName: "openyurt:yurt-coordinator:monitoring", }, cert: []byte(kubeconfigCert), key: []byte(kubeconfigKey), @@ -336,12 +336,12 @@ users: } } -// Create a fake client which have an PoolCoordinatorAPIServer SVC -func newClientWithPoolCoordinatorAPIServerSVC(objects ...runtime.Object) *fake.Clientset { +// Create a fake client which have an YurtCoordinatorAPIServer SVC +func newClientWithYurtCoordinatorAPIServerSVC(objects ...runtime.Object) *fake.Clientset { objects = append(objects, &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Namespace: PoolcoordinatorNS, - Name: PoolcoordinatorAPIServerSVC, + Namespace: YurtCoordinatorNS, + Name: YurtCoordinatorAPIServerSVC, }, Spec: corev1.ServiceSpec{ ClusterIP: "xxxx", @@ -354,7 +354,7 @@ func newClientWithPoolCoordinatorAPIServerSVC(objects ...runtime.Object) *fake.C return fake.NewSimpleClientset(objects...) } -func TestInitPoolCoordinatorCert(t *testing.T) { +func TestInitYurtCoordinatorCert(t *testing.T) { caCert, caKey, _ := NewSelfSignedCA() tests := []struct { @@ -379,7 +379,7 @@ func TestInitPoolCoordinatorCert(t *testing.T) { fake.NewSimpleClientset(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test", - Namespace: PoolcoordinatorNS, + Namespace: YurtCoordinatorNS, }, Data: map[string][]byte{ "test.crt": nil, @@ -397,7 +397,7 @@ func TestInitPoolCoordinatorCert(t *testing.T) { }, { "normal kubeconfig init", - newClientWithPoolCoordinatorAPIServerSVC(), + newClientWithYurtCoordinatorAPIServerSVC(), CertConfig{ IsKubeConfig: true, CertName: "test", @@ -416,7 +416,7 @@ func TestInitPoolCoordinatorCert(t *testing.T) { t.Parallel() t.Logf("\tTestCase: %s", st.name) { - err := initPoolCoordinatorCert(st.client, st.cfg, caCert, caKey, nil) + err := initYurtCoordinatorCert(st.client, st.cfg, caCert, caKey, nil) assert.Nil(t, err) } } diff --git a/pkg/controller/poolcoordinator/cert/secret.go b/pkg/controller/yurtcoordinator/cert/secret.go similarity index 99% rename from pkg/controller/poolcoordinator/cert/secret.go rename to pkg/controller/yurtcoordinator/cert/secret.go index de3a6bfbec8..7ce185c5258 100644 --- a/pkg/controller/poolcoordinator/cert/secret.go +++ b/pkg/controller/yurtcoordinator/cert/secret.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinatorcert +package yurtcoordinatorcert import ( "context" diff --git a/pkg/controller/poolcoordinator/cert/secret_test.go b/pkg/controller/yurtcoordinator/cert/secret_test.go similarity index 98% rename from pkg/controller/poolcoordinator/cert/secret_test.go rename to pkg/controller/yurtcoordinator/cert/secret_test.go index 601939a7f8a..48015d0d624 100644 --- a/pkg/controller/poolcoordinator/cert/secret_test.go +++ b/pkg/controller/yurtcoordinator/cert/secret_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinatorcert +package yurtcoordinatorcert import ( "testing" diff --git a/pkg/controller/poolcoordinator/cert/util.go b/pkg/controller/yurtcoordinator/cert/util.go similarity index 83% rename from pkg/controller/poolcoordinator/cert/util.go rename to pkg/controller/yurtcoordinator/cert/util.go index 8d51a5cd5aa..c3e61416277 100644 --- a/pkg/controller/poolcoordinator/cert/util.go +++ b/pkg/controller/yurtcoordinator/cert/util.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinatorcert +package yurtcoordinatorcert import ( "context" @@ -33,9 +33,9 @@ import ( "github.com/openyurtio/openyurt/pkg/yurttunnel/server/serveraddr" ) -// get poolcoordinator apiserver address +// get yurtcoordinator apiserver address func getAPIServerSVCURL(clientSet client.Interface) (string, error) { - serverSVC, err := clientSet.CoreV1().Services(PoolcoordinatorNS).Get(context.TODO(), PoolcoordinatorAPIServerSVC, metav1.GetOptions{}) + serverSVC, err := clientSet.CoreV1().Services(YurtCoordinatorNS).Get(context.TODO(), YurtCoordinatorAPIServerSVC, metav1.GetOptions{}) if err != nil { return "", err } @@ -57,9 +57,9 @@ func waitUntilSVCReady(clientSet client.Interface, serviceName string, stopCh <- // wait until get tls server Service if err = wait.PollUntil(1*time.Second, func() (bool, error) { - serverSVC, err = clientSet.CoreV1().Services(PoolcoordinatorNS).Get(context.TODO(), serviceName, metav1.GetOptions{}) + serverSVC, err = clientSet.CoreV1().Services(YurtCoordinatorNS).Get(context.TODO(), serviceName, metav1.GetOptions{}) if err == nil { - klog.Infof(Format("%s service is ready for poolcoordinator_cert_manager", serviceName)) + klog.Infof(Format("%s service is ready for yurtcoordinator_cert_manager", serviceName)) return true, nil } return false, nil @@ -69,7 +69,7 @@ func waitUntilSVCReady(clientSet client.Interface, serviceName string, stopCh <- // prepare certmanager ips = ip.ParseIPList([]string{serverSVC.Spec.ClusterIP}) - dnsnames = serveraddr.GetDefaultDomainsForSvc(PoolcoordinatorNS, serviceName) + dnsnames = serveraddr.GetDefaultDomainsForSvc(YurtCoordinatorNS, serviceName) return ips, dnsnames, nil } diff --git a/pkg/controller/poolcoordinator/cert/util_test.go b/pkg/controller/yurtcoordinator/cert/util_test.go similarity index 92% rename from pkg/controller/poolcoordinator/cert/util_test.go rename to pkg/controller/yurtcoordinator/cert/util_test.go index c78435f9f00..e459f705a28 100644 --- a/pkg/controller/poolcoordinator/cert/util_test.go +++ b/pkg/controller/yurtcoordinator/cert/util_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinatorcert +package yurtcoordinatorcert import ( "testing" @@ -97,8 +97,8 @@ func TestGetAPIServerSVCURL(t *testing.T) { normalClient := fake.NewSimpleClientset(&corev1.Service{ ObjectMeta: v1.ObjectMeta{ - Namespace: PoolcoordinatorNS, - Name: PoolcoordinatorAPIServerSVC, + Namespace: YurtCoordinatorNS, + Name: YurtCoordinatorAPIServerSVC, }, Spec: corev1.ServiceSpec{ ClusterIP: "xxxx", @@ -120,8 +120,8 @@ func TestWaitUntilSVCReady(t *testing.T) { normalClient := fake.NewSimpleClientset(&corev1.Service{ ObjectMeta: v1.ObjectMeta{ - Namespace: PoolcoordinatorNS, - Name: PoolcoordinatorAPIServerSVC, + Namespace: YurtCoordinatorNS, + Name: YurtCoordinatorAPIServerSVC, }, Spec: corev1.ServiceSpec{ ClusterIP: "xxxx", @@ -132,7 +132,7 @@ func TestWaitUntilSVCReady(t *testing.T) { }, }, }) - ips, _, err := waitUntilSVCReady(normalClient, PoolcoordinatorAPIServerSVC, stop) + ips, _, err := waitUntilSVCReady(normalClient, YurtCoordinatorAPIServerSVC, stop) assert.Equal(t, nil, err) expectIPS := ip.ParseIPList([]string{"xxxx"}) assert.Equal(t, expectIPS, ips) diff --git a/pkg/controller/poolcoordinator/cert/poolcoordinatorcert_controller.go b/pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go similarity index 69% rename from pkg/controller/poolcoordinator/cert/poolcoordinatorcert_controller.go rename to pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go index 5a83cf6a893..414e96f6839 100644 --- a/pkg/controller/poolcoordinator/cert/poolcoordinatorcert_controller.go +++ b/pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinatorcert +package yurtcoordinatorcert import ( "context" @@ -44,27 +44,27 @@ import ( ) func init() { - flag.IntVar(&concurrentReconciles, "poolcoordinatorcert-workers", concurrentReconciles, "Max concurrent workers for PoolCoordinatorCert controller.") + flag.IntVar(&concurrentReconciles, "yurtcoordinatorcert-workers", concurrentReconciles, "Max concurrent workers for YurtCoordinatorCert controller.") } var ( concurrentReconciles = 1 - PoolcoordinatorNS = "kube-system" + YurtCoordinatorNS = "kube-system" ) const ( - ControllerName = "poolcoordinatorcert" + ControllerName = "yurtcoordinatorcert" // tmp file directory for certmanager to write cert files certDir = "/tmp" - ComponentName = "yurt-controller-manager_poolcoordinator" - PoolcoordinatorAPIServerSVC = "pool-coordinator-apiserver" - PoolcoordinatorETCDSVC = "pool-coordinator-etcd" + ComponentName = "yurt-controller-manager_yurtcoordinator" + YurtCoordinatorAPIServerSVC = "yurt-coordinator-apiserver" + YurtCoordinatorETCDSVC = "yurt-coordinator-etcd" - // CA certs contains the pool-coordinator CA certs - PoolCoordinatorCASecretName = "pool-coordinator-ca-certs" - // Static certs is shared among all pool-coordinator system, which contains: + // CA certs contains the yurt-coordinator CA certs + YurtCoordinatorCASecretName = "yurt-coordinator-ca-certs" + // Static certs is shared among all yurt-coordinator system, which contains: // - ca.crt // - apiserver-etcd-client.crt // - apiserver-etcd-client.key @@ -73,30 +73,30 @@ const ( // - apiserver-kubelet-client.crt (not self signed) // - apiserver-kubelet-client.key (not self signed) // - admin.conf (kube-config) - PoolcoordinatorStaticSecretName = "pool-coordinator-static-certs" + YurtCoordinatorStaticSecretName = "yurt-coordinator-static-certs" // Dynamic certs will not be shared among clients or servers, contains: // - apiserver.crt // - apiserver.key // - etcd-server.crt // - etcd-server.key // todo: currently we only create one copy, this will be refined in the future to assign customized certs for different nodepools - PoolcoordinatorDynamicSecretName = "pool-coordinator-dynamic-certs" + YurtCoordinatorDynamicSecretName = "yurt-coordinator-dynamic-certs" // Yurthub certs shared by all yurthub, contains: // - ca.crt - // - pool-coordinator-yurthub-client.crt - // - pool-coordinator-yurthub-client.key - PoolcoordinatorYurthubClientSecretName = "pool-coordinator-yurthub-certs" - // Monitoring kubeconfig contains: monitoring kubeconfig for poolcoordinator + // - yurt-coordinator-yurthub-client.crt + // - yurt-coordinator-yurthub-client.key + YurtCoordinatorYurthubClientSecretName = "yurt-coordinator-yurthub-certs" + // Monitoring kubeconfig contains: monitoring kubeconfig for yurtcoordinator // - kubeconfig - PoolcoordinatorMonitoringKubeconfigSecretName = "pool-coordinator-monitoring-kubeconfig" + YurtCoordinatorMonitoringKubeconfigSecretName = "yurt-coordinator-monitoring-kubeconfig" - PoolcoordinatorOrg = "openyurt:pool-coordinator" - PoolcoordinatorAdminOrg = "system:masters" + YurtCoordinatorOrg = "openyurt:yurt-coordinator" + YurtCoordinatorAdminOrg = "system:masters" - PoolcoordinatorAPIServerCN = "openyurt:pool-coordinator:apiserver" - PoolcoordinatorNodeLeaseProxyClientCN = "openyurt:pool-coordinator:node-lease-proxy-client" - PoolcoordinatorETCDCN = "openyurt:pool-coordinator:etcd" - KubeConfigMonitoringClientCN = "openyurt:pool-coordinator:monitoring" + YurtCoordinatorAPIServerCN = "openyurt:yurt-coordinator:apiserver" + YurtCoordinatorNodeLeaseProxyClientCN = "openyurt:yurt-coordinator:node-lease-proxy-client" + YurtCoordinatorETCDCN = "openyurt:yurt-coordinator:etcd" + KubeConfigMonitoringClientCN = "openyurt:yurt-coordinator:monitoring" KubeConfigAdminClientCN = "cluster-admin" ) @@ -125,35 +125,35 @@ var ( allIndependentCerts = []CertConfig{ { CertName: "apiserver-etcd-client", - SecretName: PoolcoordinatorStaticSecretName, + SecretName: YurtCoordinatorStaticSecretName, IsKubeConfig: false, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - CommonName: PoolcoordinatorETCDCN, - Organization: []string{PoolcoordinatorOrg}, + CommonName: YurtCoordinatorETCDCN, + Organization: []string{YurtCoordinatorOrg}, }, { - CertName: "pool-coordinator-yurthub-client", - SecretName: PoolcoordinatorYurthubClientSecretName, + CertName: "yurt-coordinator-yurthub-client", + SecretName: YurtCoordinatorYurthubClientSecretName, IsKubeConfig: false, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, CommonName: KubeConfigAdminClientCN, - Organization: []string{PoolcoordinatorAdminOrg}, + Organization: []string{YurtCoordinatorAdminOrg}, }, } certsDependOnETCDSvc = []CertConfig{ { CertName: "etcd-server", - SecretName: PoolcoordinatorDynamicSecretName, + SecretName: YurtCoordinatorDynamicSecretName, IsKubeConfig: false, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, IPs: []net.IP{ net.ParseIP("127.0.0.1"), }, - CommonName: PoolcoordinatorETCDCN, - Organization: []string{PoolcoordinatorOrg}, + CommonName: YurtCoordinatorETCDCN, + Organization: []string{YurtCoordinatorOrg}, certInit: func(i client.Interface, c <-chan struct{}) ([]net.IP, []string, error) { - return waitUntilSVCReady(i, PoolcoordinatorETCDSVC, c) + return waitUntilSVCReady(i, YurtCoordinatorETCDSVC, c) }, }, } @@ -161,37 +161,37 @@ var ( certsDependOnAPIServerSvc = []CertConfig{ { CertName: "apiserver", - SecretName: PoolcoordinatorDynamicSecretName, + SecretName: YurtCoordinatorDynamicSecretName, IsKubeConfig: false, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - CommonName: PoolcoordinatorAPIServerCN, - Organization: []string{PoolcoordinatorOrg}, + CommonName: YurtCoordinatorAPIServerCN, + Organization: []string{YurtCoordinatorOrg}, certInit: func(i client.Interface, c <-chan struct{}) ([]net.IP, []string, error) { - return waitUntilSVCReady(i, PoolcoordinatorAPIServerSVC, c) + return waitUntilSVCReady(i, YurtCoordinatorAPIServerSVC, c) }, }, { CertName: "kubeconfig", - SecretName: PoolcoordinatorMonitoringKubeconfigSecretName, + SecretName: YurtCoordinatorMonitoringKubeconfigSecretName, IsKubeConfig: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, CommonName: KubeConfigMonitoringClientCN, - Organization: []string{PoolcoordinatorOrg}, + Organization: []string{YurtCoordinatorOrg}, // As a clientAuth cert, kubeconfig cert don't need IP&DNS to work, // but kubeconfig need this extra information to verify if it's out of date certInit: func(i client.Interface, c <-chan struct{}) ([]net.IP, []string, error) { - return waitUntilSVCReady(i, PoolcoordinatorAPIServerSVC, c) + return waitUntilSVCReady(i, YurtCoordinatorAPIServerSVC, c) }, }, { CertName: "admin.conf", - SecretName: PoolcoordinatorStaticSecretName, + SecretName: YurtCoordinatorStaticSecretName, IsKubeConfig: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, CommonName: KubeConfigAdminClientCN, - Organization: []string{PoolcoordinatorAdminOrg}, + Organization: []string{YurtCoordinatorAdminOrg}, certInit: func(i client.Interface, c <-chan struct{}) ([]net.IP, []string, error) { - return waitUntilSVCReady(i, PoolcoordinatorAPIServerSVC, c) + return waitUntilSVCReady(i, YurtCoordinatorAPIServerSVC, c) }, }, } @@ -202,10 +202,10 @@ func Format(format string, args ...interface{}) string { return fmt.Sprintf("%s: %s", ControllerName, s) } -// Add creates a new Poolcoordinatorcert Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller +// Add creates a new YurtCoordinatorcert Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(cfg *appconfig.CompletedConfig, mgr manager.Manager) error { - r := &ReconcilePoolCoordinatorCert{} + r := &ReconcileYurtCoordinatorCert{} // Create a new controller c, err := controller.New(ControllerName, mgr, controller.Options{ @@ -216,48 +216,48 @@ func Add(cfg *appconfig.CompletedConfig, mgr manager.Manager) error { } // init global variables - PoolcoordinatorNS = cfg.ComponentConfig.Generic.WorkingNamespace + YurtCoordinatorNS = cfg.ComponentConfig.Generic.WorkingNamespace - // prepare ca certs for pool coordinator + // prepare ca certs for yurt coordinator caCert, caKey, reuseCA, err := initCA(r.kubeClient) if err != nil { - return errors.Wrap(err, "init poolcoordinator failed") + return errors.Wrap(err, "init yurtcoordinator failed") } r.caCert = caCert r.caKey = caKey r.reuseCA = reuseCA // prepare all independent certs - if err := r.initPoolCoordinator(allIndependentCerts, nil); err != nil { + if err := r.initYurtCoordinator(allIndependentCerts, nil); err != nil { return err } // prepare ca cert in static secret - if err := WriteCertAndKeyIntoSecret(r.kubeClient, "ca", PoolcoordinatorStaticSecretName, r.caCert, nil); err != nil { + if err := WriteCertAndKeyIntoSecret(r.kubeClient, "ca", YurtCoordinatorStaticSecretName, r.caCert, nil); err != nil { return err } // prepare ca cert in yurthub secret - if err := WriteCertAndKeyIntoSecret(r.kubeClient, "ca", PoolcoordinatorYurthubClientSecretName, r.caCert, nil); err != nil { + if err := WriteCertAndKeyIntoSecret(r.kubeClient, "ca", YurtCoordinatorYurthubClientSecretName, r.caCert, nil); err != nil { return err } // prepare sa key pairs - if err := initSAKeyPair(r.kubeClient, "sa", PoolcoordinatorStaticSecretName); err != nil { + if err := initSAKeyPair(r.kubeClient, "sa", YurtCoordinatorStaticSecretName); err != nil { return err } - // watch pool coordinator service + // watch yurt coordinator service svcReadyPredicates := predicate.Funcs{ CreateFunc: func(evt event.CreateEvent) bool { if svc, ok := evt.Object.(*corev1.Service); ok { - return isPoolCoordinatorSvc(svc) + return isYurtCoordinatorSvc(svc) } return false }, UpdateFunc: func(evt event.UpdateEvent) bool { if svc, ok := evt.ObjectNew.(*corev1.Service); ok { - return isPoolCoordinatorSvc(svc) + return isYurtCoordinatorSvc(svc) } return false }, @@ -268,30 +268,30 @@ func Add(cfg *appconfig.CompletedConfig, mgr manager.Manager) error { return c.Watch(&source.Kind{Type: &corev1.Service{}}, &handler.EnqueueRequestForObject{}, svcReadyPredicates) } -func isPoolCoordinatorSvc(svc *corev1.Service) bool { +func isYurtCoordinatorSvc(svc *corev1.Service) bool { if svc == nil { return false } - if svc.Namespace == PoolcoordinatorNS && (svc.Name == PoolcoordinatorAPIServerSVC || svc.Name == PoolcoordinatorETCDSVC) { + if svc.Namespace == YurtCoordinatorNS && (svc.Name == YurtCoordinatorAPIServerSVC || svc.Name == YurtCoordinatorETCDSVC) { return true } return false } -var _ reconcile.Reconciler = &ReconcilePoolCoordinatorCert{} +var _ reconcile.Reconciler = &ReconcileYurtCoordinatorCert{} -// ReconcilePoolCoordinatorCert reconciles a Poolcoordinatorcert object -type ReconcilePoolCoordinatorCert struct { +// ReconcileYurtCoordinatorCert reconciles a YurtCoordinatorcert object +type ReconcileYurtCoordinatorCert struct { kubeClient client.Interface caCert *x509.Certificate caKey crypto.Signer reuseCA bool } -// InjectConfig will prepare kube client for PoolCoordinatorCert -func (r *ReconcilePoolCoordinatorCert) InjectConfig(cfg *rest.Config) error { +// InjectConfig will prepare kube client for YurtCoordinatorCert +func (r *ReconcileYurtCoordinatorCert) InjectConfig(cfg *rest.Config) error { kubeClient, err := client.NewForConfig(cfg) if err != nil { klog.Errorf("failed to create kube client, %v", err) @@ -305,13 +305,13 @@ func (r *ReconcilePoolCoordinatorCert) InjectConfig(cfg *rest.Config) error { // +kubebuilder:rbac:groups="",resources=secret,verbs=get;update;patch;create;list // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;watch;list -// todo: make customized certificate for each poolcoordinator pod -func (r *ReconcilePoolCoordinatorCert) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { +// todo: make customized certificate for each yurtcoordinator pod +func (r *ReconcileYurtCoordinatorCert) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { // Note !!!!!!!!!! // We strongly recommend use Format() to encapsulation because Format() can print logs by module // @kadisi - klog.Infof(Format("Reconcile PoolCoordinatorCert %s/%s", request.Namespace, request.Name)) + klog.Infof(Format("Reconcile YurtCoordinatorCert %s/%s", request.Namespace, request.Name)) // 1. prepare apiserver-kubelet-client cert if err := initAPIServerClientCert(r.kubeClient, ctx.Done()); err != nil { return reconcile.Result{}, err @@ -323,20 +323,20 @@ func (r *ReconcilePoolCoordinatorCert) Reconcile(ctx context.Context, request re } // 3. prepare certs based on service - if request.NamespacedName.Namespace == PoolcoordinatorNS { - if request.NamespacedName.Name == PoolcoordinatorAPIServerSVC { - return reconcile.Result{}, r.initPoolCoordinator(certsDependOnAPIServerSvc, ctx.Done()) - } else if request.NamespacedName.Name == PoolcoordinatorETCDSVC { - return reconcile.Result{}, r.initPoolCoordinator(certsDependOnETCDSvc, ctx.Done()) + if request.NamespacedName.Namespace == YurtCoordinatorNS { + if request.NamespacedName.Name == YurtCoordinatorAPIServerSVC { + return reconcile.Result{}, r.initYurtCoordinator(certsDependOnAPIServerSvc, ctx.Done()) + } else if request.NamespacedName.Name == YurtCoordinatorETCDSVC { + return reconcile.Result{}, r.initYurtCoordinator(certsDependOnETCDSvc, ctx.Done()) } } return reconcile.Result{}, nil } -func (r *ReconcilePoolCoordinatorCert) initPoolCoordinator(allSelfSignedCerts []CertConfig, stopCh <-chan struct{}) error { +func (r *ReconcileYurtCoordinatorCert) initYurtCoordinator(allSelfSignedCerts []CertConfig, stopCh <-chan struct{}) error { - klog.Infof(Format("init poolcoordinator started")) - // Prepare certs used by poolcoordinators + klog.Infof(Format("init yurtcoordinator started")) + // Prepare certs used by yurtcoordinators // prepare selfsigned certs var selfSignedCerts []CertConfig @@ -389,7 +389,7 @@ func (r *ReconcilePoolCoordinatorCert) initPoolCoordinator(allSelfSignedCerts [] // create selfsigned certs for _, certConf := range selfSignedCerts { - if err := initPoolCoordinatorCert(r.kubeClient, certConf, r.caCert, r.caKey, stopCh); err != nil { + if err := initYurtCoordinatorCert(r.kubeClient, certConf, r.caCert, r.caKey, stopCh); err != nil { klog.Errorf(Format("create cert %s fail: %v", certConf.CertName, err)) return err } @@ -399,11 +399,11 @@ func (r *ReconcilePoolCoordinatorCert) initPoolCoordinator(allSelfSignedCerts [] } // initCA is used for preparing CA certs, -// check if pool-coordinator CA already exist, if not create one +// check if yurt-coordinator CA already exist, if not create one func initCA(clientSet client.Interface) (caCert *x509.Certificate, caKey crypto.Signer, reuse bool, err error) { // try load CA cert&key from secret caCert, caKey, err = loadCertAndKeyFromSecret(clientSet, CertConfig{ - SecretName: PoolCoordinatorCASecretName, + SecretName: YurtCoordinatorCASecretName, CertName: "ca", IsKubeConfig: false, }) @@ -414,16 +414,16 @@ func initCA(clientSet client.Interface) (caCert *x509.Certificate, caKey crypto. return caCert, caKey, true, nil } else { // if ca secret does not exist, create new CA certs - klog.Infof(Format("secret(%s/%s) is not found, create new CA", PoolcoordinatorNS, PoolCoordinatorCASecretName)) + klog.Infof(Format("secret(%s/%s) is not found, create new CA", YurtCoordinatorNS, YurtCoordinatorCASecretName)) // write it into the secret caCert, caKey, err = NewSelfSignedCA() if err != nil { - return nil, nil, false, errors.Wrap(err, "fail to new self CA assets when initializing poolcoordinator") + return nil, nil, false, errors.Wrap(err, "fail to new self CA assets when initializing yurtcoordinator") } - err = WriteCertAndKeyIntoSecret(clientSet, "ca", PoolCoordinatorCASecretName, caCert, caKey) + err = WriteCertAndKeyIntoSecret(clientSet, "ca", YurtCoordinatorCASecretName, caCert, caKey) if err != nil { - return nil, nil, false, errors.Wrap(err, "fail to write CA assets into secret when initializing poolcoordinator") + return nil, nil, false, errors.Wrap(err, "fail to write CA assets into secret when initializing yurtcoordinator") } } @@ -432,14 +432,14 @@ func initCA(clientSet client.Interface) (caCert *x509.Certificate, caKey crypto. func initAPIServerClientCert(clientSet client.Interface, stopCh <-chan struct{}) error { if cert, _, err := loadCertAndKeyFromSecret(clientSet, CertConfig{ - SecretName: PoolcoordinatorStaticSecretName, + SecretName: YurtCoordinatorStaticSecretName, CertName: "apiserver-kubelet-client", IsKubeConfig: false, }); cert != nil { - klog.Infof("apiserver-kubelet-client cert has already existed in secret %s", PoolcoordinatorStaticSecretName) + klog.Infof("apiserver-kubelet-client cert has already existed in secret %s", YurtCoordinatorStaticSecretName) return nil } else if err != nil { - klog.Errorf("fail to get apiserver-kubelet-client cert in secret(%s), %v, and new cert will be created", PoolcoordinatorStaticSecretName, err) + klog.Errorf("fail to get apiserver-kubelet-client cert in secret(%s), %v, and new cert will be created", YurtCoordinatorStaticSecretName, err) } certMgr, err := certfactory.NewCertManagerFactory(clientSet).New(&certfactory.CertManagerConfig{ @@ -447,40 +447,40 @@ func initAPIServerClientCert(clientSet client.Interface, stopCh <-chan struct{}) ComponentName: fmt.Sprintf("%s-%s", ComponentName, "apiserver-client"), SignerName: certificatesv1.KubeAPIServerClientSignerName, ForServerUsage: false, - CommonName: PoolcoordinatorAPIServerCN, - Organizations: []string{PoolcoordinatorOrg}, + CommonName: YurtCoordinatorAPIServerCN, + Organizations: []string{YurtCoordinatorOrg}, }) if err != nil { return err } - return WriteCertIntoSecret(clientSet, "apiserver-kubelet-client", PoolcoordinatorStaticSecretName, certMgr, stopCh) + return WriteCertIntoSecret(clientSet, "apiserver-kubelet-client", YurtCoordinatorStaticSecretName, certMgr, stopCh) } func initNodeLeaseProxyClient(clientSet client.Interface, stopCh <-chan struct{}) error { if cert, _, err := loadCertAndKeyFromSecret(clientSet, CertConfig{ - SecretName: PoolcoordinatorYurthubClientSecretName, + SecretName: YurtCoordinatorYurthubClientSecretName, CertName: "node-lease-proxy-client", IsKubeConfig: false, }); cert != nil { - klog.Infof("node-lease-proxy-client cert has already existed in secret %s", PoolcoordinatorYurthubClientSecretName) + klog.Infof("node-lease-proxy-client cert has already existed in secret %s", YurtCoordinatorYurthubClientSecretName) return nil } else if err != nil { - klog.Errorf("fail to get node-lease-proxy-client cert in secret(%s), %v, and new cert will be created", PoolcoordinatorYurthubClientSecretName, err) + klog.Errorf("fail to get node-lease-proxy-client cert in secret(%s), %v, and new cert will be created", YurtCoordinatorYurthubClientSecretName, err) } certMgr, err := certfactory.NewCertManagerFactory(clientSet).New(&certfactory.CertManagerConfig{ CertDir: certDir, ComponentName: "yurthub", SignerName: certificatesv1.KubeAPIServerClientSignerName, - CommonName: PoolcoordinatorNodeLeaseProxyClientCN, - Organizations: []string{PoolcoordinatorOrg}, + CommonName: YurtCoordinatorNodeLeaseProxyClientCN, + Organizations: []string{YurtCoordinatorOrg}, }) if err != nil { return err } - return WriteCertIntoSecret(clientSet, "node-lease-proxy-client", PoolcoordinatorYurthubClientSecretName, certMgr, stopCh) + return WriteCertIntoSecret(clientSet, "node-lease-proxy-client", YurtCoordinatorYurthubClientSecretName, certMgr, stopCh) } // create new public/private key pair for signing service account users diff --git a/pkg/controller/poolcoordinator/cert/poolcoordinatorcert_controller_test.go b/pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go similarity index 91% rename from pkg/controller/poolcoordinator/cert/poolcoordinatorcert_controller_test.go rename to pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go index 5f995fe2b57..da1b69dde14 100644 --- a/pkg/controller/poolcoordinator/cert/poolcoordinatorcert_controller_test.go +++ b/pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package poolcoordinatorcert +package yurtcoordinatorcert import ( "testing" @@ -42,8 +42,8 @@ func TestInitCA(t *testing.T) { "CA already exist", fake.NewSimpleClientset(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: PoolCoordinatorCASecretName, - Namespace: PoolcoordinatorNS, + Name: YurtCoordinatorCASecretName, + Namespace: YurtCoordinatorNS, }, Data: map[string][]byte{ "ca.crt": []byte(caCertBytes), @@ -56,8 +56,8 @@ func TestInitCA(t *testing.T) { "CA not exist", fake.NewSimpleClientset(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: PoolCoordinatorCASecretName, - Namespace: PoolcoordinatorNS, + Name: YurtCoordinatorCASecretName, + Namespace: YurtCoordinatorNS, }, }), false, diff --git a/pkg/controller/poolcoordinator/constant/constant.go b/pkg/controller/yurtcoordinator/constant/constant.go similarity index 100% rename from pkg/controller/poolcoordinator/constant/constant.go rename to pkg/controller/yurtcoordinator/constant/constant.go diff --git a/pkg/controller/poolcoordinator/delegatelease/delegatelease_controller.go b/pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller.go similarity index 98% rename from pkg/controller/poolcoordinator/delegatelease/delegatelease_controller.go rename to pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller.go index 19d36eaa9fb..ea5f68f2886 100644 --- a/pkg/controller/poolcoordinator/delegatelease/delegatelease_controller.go +++ b/pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller.go @@ -36,9 +36,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/constant" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/utils" nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/utils" ) func init() { diff --git a/pkg/controller/poolcoordinator/delegatelease/delegatelease_controller_test.go b/pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go similarity index 94% rename from pkg/controller/poolcoordinator/delegatelease/delegatelease_controller_test.go rename to pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go index 7b4875718cc..037fd9c6fb3 100644 --- a/pkg/controller/poolcoordinator/delegatelease/delegatelease_controller_test.go +++ b/pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go @@ -21,7 +21,7 @@ import ( corev1 "k8s.io/api/core/v1" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/utils" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/utils" ) func TestTaintNode(t *testing.T) { diff --git a/pkg/controller/poolcoordinator/podbinding/podbinding_controller.go b/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go similarity index 99% rename from pkg/controller/poolcoordinator/podbinding/podbinding_controller.go rename to pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go index 471305122b9..8089cc39285 100644 --- a/pkg/controller/poolcoordinator/podbinding/podbinding_controller.go +++ b/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go @@ -33,7 +33,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" "github.com/openyurtio/openyurt/pkg/projectinfo" ) diff --git a/pkg/controller/poolcoordinator/utils/lease.go b/pkg/controller/yurtcoordinator/utils/lease.go similarity index 95% rename from pkg/controller/poolcoordinator/utils/lease.go rename to pkg/controller/yurtcoordinator/utils/lease.go index d4d3cd54907..188fd1f49d7 100644 --- a/pkg/controller/poolcoordinator/utils/lease.go +++ b/pkg/controller/yurtcoordinator/utils/lease.go @@ -20,7 +20,7 @@ package utils import ( "sync" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" ) type LeaseDelegatedCounter struct { diff --git a/pkg/controller/poolcoordinator/utils/lease_test.go b/pkg/controller/yurtcoordinator/utils/lease_test.go similarity index 100% rename from pkg/controller/poolcoordinator/utils/lease_test.go rename to pkg/controller/yurtcoordinator/utils/lease_test.go diff --git a/pkg/controller/poolcoordinator/utils/taints.go b/pkg/controller/yurtcoordinator/utils/taints.go similarity index 100% rename from pkg/controller/poolcoordinator/utils/taints.go rename to pkg/controller/yurtcoordinator/utils/taints.go diff --git a/pkg/controller/poolcoordinator/utils/taints_test.go b/pkg/controller/yurtcoordinator/utils/taints_test.go similarity index 95% rename from pkg/controller/poolcoordinator/utils/taints_test.go rename to pkg/controller/yurtcoordinator/utils/taints_test.go index 9d27032f6ab..46e15e740e1 100644 --- a/pkg/controller/poolcoordinator/utils/taints_test.go +++ b/pkg/controller/yurtcoordinator/utils/taints_test.go @@ -21,7 +21,7 @@ import ( v1 "k8s.io/api/core/v1" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" ) func TestDeleteTaintsByKey(t *testing.T) { diff --git a/pkg/webhook/pod/v1/pod_validation.go b/pkg/webhook/pod/v1/pod_validation.go index 8f2e1db129f..410e3687688 100644 --- a/pkg/webhook/pod/v1/pod_validation.go +++ b/pkg/webhook/pod/v1/pod_validation.go @@ -31,7 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/poolcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" ) const ( From 5fa5adb9c557e716724d4d4012ad00981180f749 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:48:10 +0800 Subject: [PATCH 26/93] rename pool-coordinator to yurt-coordinator for other (#1533) --- CHANGELOG.md | 16 +-- README.md | 2 +- README.zh.md | 2 +- charts/README.md | 2 +- charts/pool-coordinator/Chart.yaml | 4 +- .../pool-coordinator/templates/_helpers.tpl | 14 +- .../templates/pool-coordinator.yaml | 42 +++--- charts/pool-coordinator/values.yaml | 2 +- charts/yurthub/templates/yurthub-cfg.yaml | 2 +- ...20220307-nodepool-governance-capability.md | 132 +++++++++--------- ...0220414-multiplexing-cloud-edge-traffic.md | 28 ++-- .../20220627-yurthub-cache-refactoring.md | 14 +- docs/roadmap.md | 8 +- hack/lib/sync-charts.sh | 12 +- 14 files changed, 140 insertions(+), 140 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1824368ed0d..2c973293860 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,13 +37,13 @@ LoadBalancer service, thus getting the nodepool isolation capability of the Node - improve image build efficiency by @Congrool in https://github.com/openyurtio/openyurt/pull/1191 - support filter chain for filtering response data by @rambohe-ch in https://github.com/openyurtio/openyurt/pull/1189 - fix: re-list when target change by @LaurenceLiZhixin in https://github.com/openyurtio/openyurt/pull/1195 -- fix: pool-coordinator cannot be rescheduled when its node fails (#1212) by @AndyEWang in https://github.com/openyurtio/openyurt/pull/1218 +- fix: yurt-coordinator cannot be rescheduled when its node fails (#1212) by @AndyEWang in https://github.com/openyurtio/openyurt/pull/1218 - feat: merge yurtctl to e2e by @YTGhost in https://github.com/openyurtio/openyurt/pull/1219 - support pass bootstrap-file to yurthub by @rambohe-ch in https://github.com/openyurtio/openyurt/pull/1333 - add system proxy for docker run by @gnunu in https://github.com/openyurtio/openyurt/pull/1335 - feat: add yurtadm renew certificate command by @YTGhost in https://github.com/openyurtio/openyurt/pull/1314 - add a new way to create webhook by @JameKeal in https://github.com/openyurtio/openyurt/pull/1359 -- feat: support pool-coordinator component work in specified namespace by @y-ykcir in https://github.com/openyurtio/openyurt/pull/1355 +- feat: support yurt-coordinator component work in specified namespace by @y-ykcir in https://github.com/openyurtio/openyurt/pull/1355 - feat: add nodepool e2e by @huiwq1990 in https://github.com/openyurtio/openyurt/pull/1365 - feat: support yurt-manager work in specified namespace by @y-ykcir in https://github.com/openyurtio/openyurt/pull/1367 - support yurthub component work in specified namespace by @huweihuang in https://github.com/openyurtio/openyurt/pull/1366 @@ -54,11 +54,11 @@ LoadBalancer service, thus getting the nodepool isolation capability of the Node ### Fixes -- fix handle poolcoordinator certificates in case of restarting by @batthebee in https://github.com/openyurtio/openyurt/pull/1187 +- fix handle yurtcoordinator certificates in case of restarting by @batthebee in https://github.com/openyurtio/openyurt/pull/1187 - make rename replace old dir by @LaurenceLiZhixin in https://github.com/openyurtio/openyurt/pull/1237 - yurtadm minor version compatibility of kubelet and kubeadm by @YTGhost in https://github.com/openyurtio/openyurt/pull/1244 - delete specific iptables while testing kube-proxy by @y-ykcir in https://github.com/openyurtio/openyurt/pull/1268 -- fix yurthub dnsPolicy when using pool-coordinator by @JameKeal in https://github.com/openyurtio/openyurt/pull/1321 +- fix yurthub dnsPolicy when using yurt-coordinator by @JameKeal in https://github.com/openyurtio/openyurt/pull/1321 - fix: yurt-controller-manager reboot cannot remove taint node.openyurt.io/unschedulable (#1233) by @AndyEWang in https://github.com/openyurtio/openyurt/pull/1337 - fix daemonSet pod updater pointer error by @JameKeal in https://github.com/openyurtio/openyurt/pull/1340 - bugfix for yurtappset by @theonefx in https://github.com/openyurtio/openyurt/pull/1391 @@ -104,7 +104,7 @@ giving feedback, helping users in community group, etc. The original edge autonomy feature can make the pods on nodes un-evicted even if node crashed by adding annotation to node, and this feature is recommended to use for scenarios that pods should bind to node without recreation. After improving edge autonomy capability, when the reason of node NotReady is cloud-edge network off, pods will not be evicted -because leader yurthub will help these offline nodes to proxy their heartbeats to the cloud via pool-coordinator component, +because leader yurthub will help these offline nodes to proxy their heartbeats to the cloud via yurt-coordinator component, and pods will be evicted and recreated on other ready node if node crashed. By the way, the original edge autonomy capability by annotating node (with node.beta.openyurt.io/autonomy) will be kept as it is, @@ -113,9 +113,9 @@ enable the original edge autonomy capability for specified pod. **Reduce the control-plane traffic between cloud and edge** -Based on the Pool-Coordinator in the nodePool, A leader Yurthub will be elected in the nodePool. Leader Yurthub will -list/watch pool-scope data(like endpoints/endpointslices) from cloud and write into pool-coordinator. then all components(like kube-proxy/coredns) -in the nodePool will get pool-scope data from pool-coordinator instead of cloud kube-apiserver, so large volume control-plane traffic +Based on the Yurt-Coordinator in the nodePool, A leader Yurthub will be elected in the nodePool. Leader Yurthub will +list/watch pool-scope data(like endpoints/endpointslices) from cloud and write into yurt-coordinator. then all components(like kube-proxy/coredns) +in the nodePool will get pool-scope data from yurt-coordinator instead of cloud kube-apiserver, so large volume control-plane traffic will be reduced. **Use raven component to replace yurt-tunnel component** diff --git a/README.md b/README.md index 71b8affc667..811c3c4c0a5 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ The above figure demonstrates the core OpenYurt architecture. The major componen - **[YurtHub](https://openyurt.io/docs/next/core-concepts/yurthub)**: YurtHub runs on worker nodes as static pod and serves as a node sidecar to handle requests that comes from components (like Kubelet, Kubeproxy, etc.) on worker nodes to kube-apiserver. - **[Yurt-Manager](https://github.com/openyurtio/openyurt/tree/master/cmd/yurt-manager)**: include all controllers and webhooks for edge. - **[Raven-Agent](https://openyurt.io/docs/next/core-concepts/raven)**: It is focused on edge-edge and edge-cloud communication in OpenYurt, and provides layer 3 network connectivity among pods in different physical regions, as there are in one vanilla Kubernetes cluster. -- **Pool-Coordinator**: One instance of Pool-Coordinator is deployed in every edge NodePool, and in conjunction with YurtHub to provide heartbeat delegation, cloud-edge traffic multiplexing abilities, etc. +- **Yurt-Coordinator**: One instance of Yurt-Coordinator is deployed in every edge NodePool, and in conjunction with YurtHub to provide heartbeat delegation, cloud-edge traffic multiplexing abilities, etc. In addition, OpenYurt also includes auxiliary controllers for integration and customization purposes. diff --git a/README.zh.md b/README.zh.md index 05414f178d0..ba0cc8b4b13 100644 --- a/README.zh.md +++ b/README.zh.md @@ -48,7 +48,7 @@ OpenYurt 遵循经典的云边一体化架构。 - **[YurtHub](https://openyurt.io/zh/docs/next/core-concepts/yurthub/)**:YurtHub 以静态 pod 模式在工作节点上运行,它作为节点的 Sidecar 处理所有来自工作节点上的组件(如 Kubelet, Kubeproxy 等)到 kube-apiserver 的请求。 - **[Yurt-Manager](https://github.com/openyurtio/openyurt/tree/master/cmd/yurt-manager)**:包括所有云边协同场景下的Controllers和Webhooks。 - **[Raven-Agent](https://openyurt.io/docs/next/core-concepts/raven)**: 它用于处理 OpenYurt 中的云边,边边间的跨公网通信。 主要在不同物理区域的 pod 之间提供第 3 层网络连接,就像在一个 vanilla Kubernetes 集群中一样。 -- **Pool-Coordinator**:每个边缘 NodePool 中会自动部署一个 Pool-Coordinator 实例,它联合 YurtHub 为节点池提供心跳代理、云边缘流量复用等能力。 +- **Yurt-Coordinator**:每个边缘 NodePool 中会自动部署一个 Yurt-Coordinator 实例,它联合 YurtHub 为节点池提供心跳代理、云边缘流量复用等能力。 此外,OpenYurt 还包括用于集成和定制的辅助控制器。 diff --git a/charts/README.md b/charts/README.md index 9969d1d6f81..c493f5089b6 100644 --- a/charts/README.md +++ b/charts/README.md @@ -1,4 +1,4 @@ OpenYurt Charts contains three OpenYurt components: - yurt-manager - yurthub -- pool-coordinator \ No newline at end of file +- yurt-coordinator \ No newline at end of file diff --git a/charts/pool-coordinator/Chart.yaml b/charts/pool-coordinator/Chart.yaml index bd69b523971..8676106d32f 100644 --- a/charts/pool-coordinator/Chart.yaml +++ b/charts/pool-coordinator/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: pool-coordinator -description: A Helm chart for OpenYurt pool-coordinator component +name: yurt-coordinator +description: A Helm chart for OpenYurt yurt-coordinator component # A chart can be either an 'application' or a 'library' chart. # diff --git a/charts/pool-coordinator/templates/_helpers.tpl b/charts/pool-coordinator/templates/_helpers.tpl index db878a7556d..e9e0bf6dcff 100644 --- a/charts/pool-coordinator/templates/_helpers.tpl +++ b/charts/pool-coordinator/templates/_helpers.tpl @@ -1,23 +1,23 @@ {{/* Expand the name of the chart. */}} -{{- define "pool-coordinator.name" -}} +{{- define "yurt-coordinator.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} -{{- define "pool-coordinator.chart" -}} +{{- define "yurt-coordinator.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "pool-coordinator.labels" -}} -helm.sh/chart: {{ include "pool-coordinator.chart" . }} -{{ include "pool-coordinator.selectorLabels" . }} +{{- define "yurt-coordinator.labels" -}} +helm.sh/chart: {{ include "yurt-coordinator.chart" . }} +{{ include "yurt-coordinator.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -27,7 +27,7 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "pool-coordinator.selectorLabels" -}} -app.kubernetes.io/name: {{ include "pool-coordinator.name" . }} +{{- define "yurt-coordinator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "yurt-coordinator.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} \ No newline at end of file diff --git a/charts/pool-coordinator/templates/pool-coordinator.yaml b/charts/pool-coordinator/templates/pool-coordinator.yaml index a21b8c8eb7a..d2ffcea4cde 100644 --- a/charts/pool-coordinator/templates/pool-coordinator.yaml +++ b/charts/pool-coordinator/templates/pool-coordinator.yaml @@ -1,12 +1,12 @@ apiVersion: v1 kind: Service metadata: - name: pool-coordinator-apiserver + name: yurt-coordinator-apiserver namespace: {{ .Release.Namespace }} annotations: openyurt.io/topologyKeys: openyurt.io/nodepool labels: - {{- include "pool-coordinator.labels" . | nindent 4 }} + {{- include "yurt-coordinator.labels" . | nindent 4 }} spec: type: ClusterIP ports: @@ -15,17 +15,17 @@ spec: protocol: TCP name: https selector: - {{- include "pool-coordinator.selectorLabels" . | nindent 4 }} + {{- include "yurt-coordinator.selectorLabels" . | nindent 4 }} --- apiVersion: v1 kind: Service metadata: - name: pool-coordinator-etcd + name: yurt-coordinator-etcd namespace: {{ .Release.Namespace }} annotations: openyurt.io/topologyKeys: openyurt.io/nodepool labels: - {{- include "pool-coordinator.labels" . | nindent 4 }} + {{- include "yurt-coordinator.labels" . | nindent 4 }} spec: type: ClusterIP ports: @@ -34,19 +34,19 @@ spec: protocol: TCP name: https selector: - {{- include "pool-coordinator.selectorLabels" . | nindent 4 }} + {{- include "yurt-coordinator.selectorLabels" . | nindent 4 }} --- apiVersion: apps.openyurt.io/v1alpha1 kind: YurtAppDaemon metadata: - name: pool-coordinator + name: yurt-coordinator namespace: {{ .Release.Namespace }} labels: - {{- include "pool-coordinator.labels" . | nindent 4 }} + {{- include "yurt-coordinator.labels" . | nindent 4 }} spec: selector: matchLabels: - {{- include "pool-coordinator.selectorLabels" . | nindent 6 }} + {{- include "yurt-coordinator.selectorLabels" . | nindent 6 }} nodepoolSelector: matchLabels: openyurt.io/node-pool-type: "edge" @@ -54,16 +54,16 @@ spec: deploymentTemplate: metadata: labels: - {{- include "pool-coordinator.labels" . | nindent 10 }} + {{- include "yurt-coordinator.labels" . | nindent 10 }} spec: replicas: 1 selector: matchLabels: - {{- include "pool-coordinator.selectorLabels" . | nindent 12 }} + {{- include "yurt-coordinator.selectorLabels" . | nindent 12 }} template: metadata: labels: - {{- include "pool-coordinator.labels" . | nindent 14 }} + {{- include "yurt-coordinator.labels" . | nindent 14 }} spec: containers: - command: @@ -133,7 +133,7 @@ spec: terminationMessagePolicy: File volumeMounts: - mountPath: /etc/kubernetes/pki - name: pool-coordinator-certs + name: yurt-coordinator-certs readOnly: true - command: - etcd @@ -170,7 +170,7 @@ spec: - mountPath: /var/lib/etcd name: etcd-data - mountPath: /etc/kubernetes/pki - name: pool-coordinator-certs + name: yurt-coordinator-certs readOnly: true dnsPolicy: ClusterFirst {{- if .Values.imagePullSecrets }} @@ -196,15 +196,15 @@ spec: defaultMode: 420 sources: - secret: - name: pool-coordinator-dynamic-certs + name: yurt-coordinator-dynamic-certs - secret: - name: pool-coordinator-static-certs - name: pool-coordinator-certs + name: yurt-coordinator-static-certs + name: yurt-coordinator-certs --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: openyurt:pool-coordinator:node-lease-proxy-client + name: openyurt:yurt-coordinator:node-lease-proxy-client rules: - apiGroups: - "coordination.k8s.io" @@ -217,12 +217,12 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: openyurt:pool-coordinator:node-lease-proxy-client + name: openyurt:yurt-coordinator:node-lease-proxy-client roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: openyurt:pool-coordinator:node-lease-proxy-client + name: openyurt:yurt-coordinator:node-lease-proxy-client subjects: - apiGroup: rbac.authorization.k8s.io kind: User - name: openyurt:pool-coordinator:node-lease-proxy-client + name: openyurt:yurt-coordinator:node-lease-proxy-client diff --git a/charts/pool-coordinator/values.yaml b/charts/pool-coordinator/values.yaml index fab7084fa02..30919fffa5c 100644 --- a/charts/pool-coordinator/values.yaml +++ b/charts/pool-coordinator/values.yaml @@ -1,4 +1,4 @@ -# Default values for pool-coordinator. +# Default values for yurt-coordinator. # This is a YAML-formatted file. # Declare variables to be passed into your templates. diff --git a/charts/yurthub/templates/yurthub-cfg.yaml b/charts/yurthub/templates/yurthub-cfg.yaml index 6f026200487..478bebabb54 100644 --- a/charts/yurthub/templates/yurthub-cfg.yaml +++ b/charts/yurthub/templates/yurthub-cfg.yaml @@ -29,7 +29,7 @@ rules: resources: - secrets resourceNames: - - pool-coordinator-yurthub-certs + - yurt-coordinator-yurthub-certs verbs: - list - watch diff --git a/docs/proposals/20220307-nodepool-governance-capability.md b/docs/proposals/20220307-nodepool-governance-capability.md index 009226b3ddd..15f70acf222 100644 --- a/docs/proposals/20220307-nodepool-governance-capability.md +++ b/docs/proposals/20220307-nodepool-governance-capability.md @@ -26,10 +26,10 @@ status: provisional - [Architecture](#architecture) - [Implementation Details](#implementation-details) - [Coordinator-Controller](#coordinator-controller) - - [Pool-Coordinator](#pool-coordinator) + - [Yurt-Coordinator](#yurt-coordinator) - [Operations and Maintenance in NodePool](#operations-and-maintenance-in-nodepool) - - [Write Resources to pool-coordinator](#write-resources-to-pool-coordinator) - - [Kubeconfig for users to access pool-coordinator](#kubeconfig-for-users-to-access-pool-coordinator) + - [Write Resources to yurt-coordinator](#write-resources-to-yurt-coordinator) + - [Kubeconfig for users to access yurt-coordinator](#kubeconfig-for-users-to-access-yurt-coordinator) - [NodePool Autonomy](#nodepool-autonomy) - [User Stories](#user-stories) - [Other Problems](#other-problems) @@ -93,93 +93,93 @@ That is to say, the high availability of services in the node pool is not involv ### Architecture -To provide `NodePool governance capability`, we add a component `pool-coordinator` to the NodePool. -pool-coordinator should not replace the kube-apiserver on the cloud, but only provide governance capabilities in NodePool scope. +To provide `NodePool governance capability`, we add a component `yurt-coordinator` to the NodePool. +yurt-coordinator should not replace the kube-apiserver on the cloud, but only provide governance capabilities in NodePool scope. Its structure is as follows: ![nodepool-governance](../img/nodepool-governance/img-1.png) -- The `coordinator-controller` deployed in the cloud manages the pool-coordinator through YurtAppSet. -When NodePool enables the NodePool governance capability, pool-coordinator will be automatically deployed by coordinator-controller in NodePool. -- When pool-coordinator starts, all YurtHubs in NodePool upload the node scope resources cached on their nodes to pool-coordinator, +- The `coordinator-controller` deployed in the cloud manages the yurt-coordinator through YurtAppSet. +When NodePool enables the NodePool governance capability, yurt-coordinator will be automatically deployed by coordinator-controller in NodePool. +- When yurt-coordinator starts, all YurtHubs in NodePool upload the node scope resources cached on their nodes to yurt-coordinator, including pod, configmap, secrets, service, node, lease, serviceaccount, etc. - When the edge node can connect to the master, YurtHub directly accesses the kube-apiserver, -caches the data returned by the cloud locally, and updates the data to pool-coordinator in time. +caches the data returned by the cloud locally, and updates the data to yurt-coordinator in time. This ensures that users can obtain the latest resources when operating in the node pool (such as kubectl get). - When the node can connect to the cloud, YurtHub sends node lease to the cloud. However, when the node is disconnected from the cloud, YurtHub adds an agent forwarding Annotation to the node lease and -sends it to pool-coordinator, then leader YurtHub forwards it to the cloud in real time. -- When NodePool disables the NodePool governance capability, coordinator-controller will clean up the pool-coordinator belonging to this NodePool. +sends it to yurt-coordinator, then leader YurtHub forwards it to the cloud in real time. +- When NodePool disables the NodePool governance capability, coordinator-controller will clean up the yurt-coordinator belonging to this NodePool. ### Implementation Details #### Coordinator-Controller -coordinator-controller is used to manage the life cycle of the pool-coordinator in each NodePool and deployed as deployment. +coordinator-controller is used to manage the life cycle of the yurt-coordinator in each NodePool and deployed as deployment. coordinator-controller can be described as: - Initialize work at startup: 1. coordinator-controller will block until the YurtAppSet CRDs are ready. -2. coordinator-controller prepares the client certificate to access the kubelet for pool-coordinator, -saves the certificate in secret and mounts it to pool-coordinator. -Note that all pool-coordinators can share this client certificate. +2. coordinator-controller prepares the client certificate to access the kubelet for yurt-coordinator, +saves the certificate in secret and mounts it to yurt-coordinator. +Note that all yurt-coordinators can share this client certificate. 3. coordinator-controller prepares the client certificate for forwarding node lease to cloud by yurthub, saves the client certificate in secret and will be used by leader yurthub. -4. coordinator-controller creates service for pool-coordinator. -5. coordinator-controller generates a YurtAppSet Object for managing pool-coordinator, and set field 'pool' to empty. +4. coordinator-controller creates service for yurt-coordinator. +5. coordinator-controller generates a YurtAppSet Object for managing yurt-coordinator, and set field 'pool' to empty. - Reconcile: -1. coordinator-controller will list/watch PoolCoordinator CR. When user creates a PoolCoordinator CR, coordinator-controller adds the NodePool information -to YurtAppSet, so that a pool-coordinator instance will be deployed in the NodePool. Note that the coordinator-controller refuses to -deploy the pool-coordinator when the number of nodes in the NodePool is less than 3, or if a pool-coordinator has been deployed in the NodePool. -2. When pool-coordinator is scheduled, coordinator-controller prepares the tls server certificate for pool-coordinator, -saves the certificate in secret and mounts it to pool-coordinator. Note that the tls server certificate for each pool-coordinator is different -because certificate includes the pool-coordinator service clusterIP and the node IP. -3. coordinator-controller generates kubeconfig for users to access pool-coordinator. The server address in kubeconfig is set to +1. coordinator-controller will list/watch YurtCoordinator CR. When user creates a YurtCoordinator CR, coordinator-controller adds the NodePool information +to YurtAppSet, so that a yurt-coordinator instance will be deployed in the NodePool. Note that the coordinator-controller refuses to +deploy the yurt-coordinator when the number of nodes in the NodePool is less than 3, or if a yurt-coordinator has been deployed in the NodePool. +2. When yurt-coordinator is scheduled, coordinator-controller prepares the tls server certificate for yurt-coordinator, +saves the certificate in secret and mounts it to yurt-coordinator. Note that the tls server certificate for each yurt-coordinator is different +because certificate includes the yurt-coordinator service clusterIP and the node IP. +3. coordinator-controller generates kubeconfig for users to access yurt-coordinator. The server address in kubeconfig is set to https://{nodeIP}:10270. In addition, the client certificate authority in kubeconfig should be restricted. For details, -please refer to the [kubeconfig of pool-coordinator](#kubeconfig-for-users-to-access-pool-coordinator). -4. When the pool-coordinator is rebuilt, coordinator-controller will clean up and rebuild the tls server certificate. -5. When PoolCoordinator CR is deleted, coordinator-controller will delete the NodePool information in YurtAppSet. It also cleans up -the certificates of pool-coordinator(tls server certificate and kubeconfig). +please refer to the [kubeconfig of yurt-coordinator](#kubeconfig-for-users-to-access-yurt-coordinator). +4. When the yurt-coordinator is rebuilt, coordinator-controller will clean up and rebuild the tls server certificate. +5. When YurtCoordinator CR is deleted, coordinator-controller will delete the NodePool information in YurtAppSet. It also cleans up +the certificates of yurt-coordinator(tls server certificate and kubeconfig). Since node autonomy already supported by OpenYurt and [NodePool Autonomy](#nodepool-autonomy) are applicable to different scenarios, both abilities cannot be enabled at the same time. We do it through admission webhook. -#### Pool-Coordinator +#### Yurt-Coordinator -pool-coordinator will store various resources in the node pool, including node, pod, service, endpoints, endpointslices, etc. -pool-coordinator is managed by YurtAppSet and deploys kube-apiserver and etcd in one pod. Here resources in etcd will be stored in memory instead of disk. +yurt-coordinator will store various resources in the node pool, including node, pod, service, endpoints, endpointslices, etc. +yurt-coordinator is managed by YurtAppSet and deploys kube-apiserver and etcd in one pod. Here resources in etcd will be stored in memory instead of disk. ```go -// PoolCoordinator CRD -type PoolCoordinator Struct { +// YurtCoordinator CRD +type YurtCoordinator Struct { metav1.TypeMeta metav1.ObjectMeta - Spec PoolCoordinatorSpec - Status PoolCoordinatorStatus + Spec YurtCoordinatorSpec + Status YurtCoordinatorStatus } -type PoolCoordinatorSpec struct { - // Version of pool-coordinator, which corresponding to the Kubernetes version +type YurtCoordinatorSpec struct { + // Version of yurt-coordinator, which corresponding to the Kubernetes version Version string - // The NodePool managed by pool-coordinator. + // The NodePool managed by yurt-coordinator. NodePool string } -type PoolCoordinatorStatus struct { - // The node where pool-coordinator is located. +type YurtCoordinatorStatus struct { + // The node where yurt-coordinator is located. NodeName string - // Conditions represent the status of pool-coordinator, which is filled by the coordinator-controller. - Conditions []PoolCoordinatorCondition + // Conditions represent the status of yurt-coordinator, which is filled by the coordinator-controller. + Conditions []YurtCoordinatorCondition // DelegatedNodes are the nodes in the node pool that are disconnected from the cloud. DelegatedNodes []string - // OutsidePoolNodes are nodes in the node pool that cannot connect to pool-coordinator. + // OutsidePoolNodes are nodes in the node pool that cannot connect to yurt-coordinator. OutsidePoolNodes []string } -type PoolCoordinatorCondition struct { - Type PoolCoordinatorConditionType +type YurtCoordinatorCondition struct { + Type YurtCoordinatorConditionType Status ConditionStatus LastProbeTime metav1.Time LastTransitionTime metav1.Time @@ -187,16 +187,16 @@ type PoolCoordinatorCondition struct { Message string } -type PoolCoordinatorConditionType string +type YurtCoordinatorConditionType string const ( - // PoolCoordinatorPending indicates that the deployment of pool-coordinator is blocked. + // YurtCoordinatorPending indicates that the deployment of yurt-coordinator is blocked. //This happens, for example, if the number of nodes in the node pool is less than 3. - PoolCoordinatorPending PoolCoordinatorConditionType = "Pending" - // PoolCoordinatorCertsReady indicates that the certificate used by pool-coordinator is ready. - PoolCoordinatorCertsReady PoolCoordinatorConditionType = "CertsReady" - // PoolCoordinatorReady indicates that pool-coordinator is ready. - PoolCoordinatorReady PoolCoordinatorConditionType = "Ready" + YurtCoordinatorPending YurtCoordinatorConditionType = "Pending" + // YurtCoordinatorCertsReady indicates that the certificate used by yurt-coordinator is ready. + YurtCoordinatorCertsReady YurtCoordinatorConditionType = "CertsReady" + // YurtCoordinatorReady indicates that yurt-coordinator is ready. + YurtCoordinatorReady YurtCoordinatorConditionType = "Ready" ) type ConditionStatus string @@ -210,43 +210,43 @@ const ( - Https Server Certificate -coordinator-controller prepares the tls server certificate for kube-apiserer in pool-coordinator and mounts it into the pod through -a secret by using the patch feature of YurtAppSet. pool-coordinator runs in HostNetWork mode, +coordinator-controller prepares the tls server certificate for kube-apiserer in yurt-coordinator and mounts it into the pod through +a secret by using the patch feature of YurtAppSet. yurt-coordinator runs in HostNetWork mode, and the https server listening address is: https://{nodeIP}:10270. - Service Discovery -pool-coordinator provides services by ClusterIP Service in Kubernetes, and all pool-coordinators share the service IP. +yurt-coordinator provides services by ClusterIP Service in Kubernetes, and all yurt-coordinators share the service IP. -In order to ensure that pool-coordinator only serves nodes in the same node pool, the annotation of service topology needs to be added to the pool-coordinator service. +In order to ensure that yurt-coordinator only serves nodes in the same node pool, the annotation of service topology needs to be added to the yurt-coordinator service. #### Operations and Maintenance in NodePool -In terms of operation and maintenance, pool-coordinator supports two types of requests: +In terms of operation and maintenance, yurt-coordinator supports two types of requests: - GET requests for resources in NodePool, such as nodes, pods, etc. - Native kubernetes operation and maintenance requests for pods in NodePool, such as kubectl logs/exec/cp/attach, etc. To support the above capabilities, the following problems need to be solved: -##### Write Resources to pool-coordinator +##### Write Resources to yurt-coordinator In OpenYurt, the data flow between cloud and edge is: kube-apiserver --> yurthub --> kubelet (and other clients). -In order to ensure data consistency and efficiency, pool-coordinator reuses the current data flow of OpenYurt. -The data flow of pool-coordinator is: kube-apiserver --> yurthub --> pool-coordinator. Data in pool-coordinator is written by each YurtHub. +In order to ensure data consistency and efficiency, yurt-coordinator reuses the current data flow of OpenYurt. +The data flow of yurt-coordinator is: kube-apiserver --> yurthub --> yurt-coordinator. Data in yurt-coordinator is written by each YurtHub. -YurtHub updates data to pool-coordinator, so it requires Create/Update permissions for resources. After the pool-coordinator starts, +YurtHub updates data to yurt-coordinator, so it requires Create/Update permissions for resources. After the yurt-coordinator starts, we need to prepare the CRD NodePool, clusterrolebinding associated with `system:nodes` group and admin clusterrole in the kube-apiserver. This ensures that YurtHub can successfully write to etcd using node client certificate. ![](../img/nodepool-governance/img-2.png) -##### Kubeconfig for users to access pool-coordinator +##### Kubeconfig for users to access yurt-coordinator Kubeconfig is generated by coordinator-controller, and the organization configuration of client certificate is: `openyurt:coordinators`. In addition, add the get permission of the resource and the operation and maintenance permissions(logs/exec) for the -group `openyurt:coordinators` to kube-apiserver of pool-coordinator. +group `openyurt:coordinators` to kube-apiserver of yurt-coordinator. #### NodePool Autonomy @@ -257,7 +257,7 @@ In the same node pool, when the node is disconnected from the cloud,the leader Y the node lease to the cloud. It can be described as: ![](../img/nodepool-governance/img-3.png) ![](../img/nodepool-governance/img-4.png) -**Note:** If the lease of pool-coordinator node is also need to be forwarded, the leader YurtHub will give priority to forwarding Node leases of its node. +**Note:** If the lease of yurt-coordinator node is also need to be forwarded, the leader YurtHub will give priority to forwarding Node leases of its node. The policy of the cloud controller is as follows: @@ -275,5 +275,5 @@ in normal nodes when node downtime. #### H/A Consideration -Consider that when the pool-coordinator fails, each component can be fully rolled back. -Therefore, in order to save resources, only one pool-coordinator instance is deployed in each NodePool. \ No newline at end of file +Consider that when the yurt-coordinator fails, each component can be fully rolled back. +Therefore, in order to save resources, only one yurt-coordinator instance is deployed in each NodePool. \ No newline at end of file diff --git a/docs/proposals/20220414-multiplexing-cloud-edge-traffic.md b/docs/proposals/20220414-multiplexing-cloud-edge-traffic.md index 74861a1f4c1..a54eea41676 100644 --- a/docs/proposals/20220414-multiplexing-cloud-edge-traffic.md +++ b/docs/proposals/20220414-multiplexing-cloud-edge-traffic.md @@ -17,7 +17,7 @@ status: provisional Refer to the [OpenYurt Glossary](https://github.com/openyurtio/openyurt/blob/master/docs/proposals/00_openyurt-glossary.md). ## Summary -In OpenYurt cluster, the traffic between cloud and edge should go through public network. so it is very valuable to reduce the traffic between cloud and edge. In the proposal [#772]([https://github.com/openyurtio/openyurt/pull/772](https://github.com/openyurtio/openyurt/pull/772)), pool-coordinator pod will be deployed in each node pool for storing metadata as a kv storage. In this proposal, we will approach a way to reduce cloud-edge traffic based on `pool-coordinator`and ensure the consistency of edge side metadata. +In OpenYurt cluster, the traffic between cloud and edge should go through public network. so it is very valuable to reduce the traffic between cloud and edge. In the proposal [#772]([https://github.com/openyurtio/openyurt/pull/772](https://github.com/openyurtio/openyurt/pull/772)), yurt-coordinator pod will be deployed in each node pool for storing metadata as a kv storage. In this proposal, we will approach a way to reduce cloud-edge traffic based on `yurt-coordinator`and ensure the consistency of edge side metadata. ## Motivation In a large-scale OpenYurt cluster (eg: node > 1k, pod > 20k, service > 1k), since coredns/kube-proxy on the edge nodes lists and watches all endpoints/endpointslices, the cloud-edge traffic will increase rapidly if pods are deleted and rebuilt due to business upgrade. In addition, frequent node state switching(Ready/NotReady) will cause NodePool updates, which will also lead to a huge increase in cloud-edge traffic. @@ -44,24 +44,24 @@ node scope data will not be involved in this solution, every node in NodePool ne - The data of each node in the NodePool is unique, such as pods, secrets, configmaps, etc. ### Architecture -pool scope data(endpoints, endpointslices) is written to the pool-coordinator by the leader YurtHub. -It is ensured that the pool scope data in the pool-coordinator is the latest version, so list/watch requests for pool scope data from standby yurthub can be obtained from the pool-coordinator and no longer obtained from the cloud, which can greatly reduce cloud-edge traffic. +pool scope data(endpoints, endpointslices) is written to the yurt-coordinator by the leader YurtHub. +It is ensured that the pool scope data in the yurt-coordinator is the latest version, so list/watch requests for pool scope data from standby yurthub can be obtained from the yurt-coordinator and no longer obtained from the cloud, which can greatly reduce cloud-edge traffic. ![img.png](../img/multiplexing-traffic/img1.png) The process can be described as: - **Step1**: At the beginning, All YurtHubs send list/watch requests of pool scope data to the cloud. -- **Step2**: When the pool-coordinator is started and the leader YurtHub is elected, the leader YurtHub creates new list/watch requests of pool scope data and writes the response data to the pool-coordinator. Leader Yurthub will write a completion flag(a configmap) into pool-coordinator for specifying pool scope data has synced and all yurthub will be notified that it's the time to list/watch pool scope data from pool-coordinator. At this time, each YurtHub still sends list/watch requests to the cloud. -- **Step3**: After leader YurtHub finishes writing pool scope data, each YurtHub stops sending list/watch requests of pool scope data to the cloud and redirect list/watch requests to pool-coordinator. At this time, only the leader YurtHub still maintains list/watch requests with the cloud and keeps writing data to the pool-coordinator. -- **Step4**: If the pool-coordinator goes offline, that is, NodePool governance capabilities is disabled, YurtHub redirect the List/Watch requests of pool scope data to the cloud again. +- **Step2**: When the yurt-coordinator is started and the leader YurtHub is elected, the leader YurtHub creates new list/watch requests of pool scope data and writes the response data to the yurt-coordinator. Leader Yurthub will write a completion flag(a configmap) into yurt-coordinator for specifying pool scope data has synced and all yurthub will be notified that it's the time to list/watch pool scope data from yurt-coordinator. At this time, each YurtHub still sends list/watch requests to the cloud. +- **Step3**: After leader YurtHub finishes writing pool scope data, each YurtHub stops sending list/watch requests of pool scope data to the cloud and redirect list/watch requests to yurt-coordinator. At this time, only the leader YurtHub still maintains list/watch requests with the cloud and keeps writing data to the yurt-coordinator. +- **Step4**: If the yurt-coordinator goes offline, that is, NodePool governance capabilities is disabled, YurtHub redirect the List/Watch requests of pool scope data to the cloud again. The following special cases may occur: -**Condition1**: If the leader YurtHub changes, the new leader will take over all the work: send list/watch requests of pool scope data to the cloud, and write data to pool-coordinator. the former leader Yurthub stop the list/watch requests to the cloud. +**Condition1**: If the leader YurtHub changes, the new leader will take over all the work: send list/watch requests of pool scope data to the cloud, and write data to yurt-coordinator. the former leader Yurthub stop the list/watch requests to the cloud. -**Condition2**: If the pool-coordinator restarts/rebuilds, during the restart of the pool-coordinator, YurtHub will still try to connect with the pool-coordinator for a certain period of time. After pool-coordinator starts,Step2-3 will be executed. +**Condition2**: If the yurt-coordinator restarts/rebuilds, during the restart of the yurt-coordinator, YurtHub will still try to connect with the yurt-coordinator for a certain period of time. After yurt-coordinator starts,Step2-3 will be executed. -### Write Metadata into pool-coordinator -Pool scope data and node scope data are stored in pool-coordinator. Node scope data is written by each YurtHub, while pool scope data is written by only leader YurtHub. +### Write Metadata into yurt-coordinator +Pool scope data and node scope data are stored in yurt-coordinator. Node scope data is written by each YurtHub, while pool scope data is written by only leader YurtHub. Leader YurtHub is elected by all YurtHubs in node pool(like kube-controller-manager). Because it needs to ensure that the leader YurtHub can get the real-time pool scope data from kube-apiserver, the leader YurtHub and the cloud must be connected. When the leader is disconnected from the cloud, other YurtHubs connected to the cloud become the leader. ![img.png](../img/multiplexing-traffic/img2.png) @@ -74,14 +74,14 @@ Leader YurtHub is elected by all YurtHubs in node pool(like kube-controller-mana #### Data Traffic Conclusion | | node scope data | pool scope data | | --- | --- | --- | -| cloud-edge network on | cloud kube-apiserver --> yurthub --> kubelet/kube-proxy | cloud kube-apiserver --> leader yurthub --> pool-coordinator --> every yurthub --> kubelet/kube-proxy | -| cloud-edge network off | edge node local storage --> yurthub --> kubelet/kube-proxy | 1. a completion flag exists in pool-coordinator: pool-coordinator--> yurthub --> kubelet/kube-proxy
2. a completion flag does not exist in pool-coordinator: edge node local storage --> yurthub --> kubelet/kube-proxy | +| cloud-edge network on | cloud kube-apiserver --> yurthub --> kubelet/kube-proxy | cloud kube-apiserver --> leader yurthub --> yurt-coordinator --> every yurthub --> kubelet/kube-proxy | +| cloud-edge network off | edge node local storage --> yurthub --> kubelet/kube-proxy | 1. a completion flag exists in yurt-coordinator: yurt-coordinator--> yurthub --> kubelet/kube-proxy
2. a completion flag does not exist in yurt-coordinator: edge node local storage --> yurthub --> kubelet/kube-proxy | #### Pool Scope Data Protection -If resources for Endpoints/EndpointSlices of an empty list are returned from pool-coordinator for some unknown reason, yurthub directly hacks the return of the list request to prevent data cleanup on edge nodes and return error to the clients. +If resources for Endpoints/EndpointSlices of an empty list are returned from yurt-coordinator for some unknown reason, yurthub directly hacks the return of the list request to prevent data cleanup on edge nodes and return error to the clients. #### Service Topology Notification -Through the traffic reuse of Pool Scope Data, some nodes in NodePool are disconnected from the cloud network. However, the nodes that are disconnected can also obtain the latest Endpoints/Endpointslices data (because they belong to pool Scope data) through pool-coordinator, so the service topology changes in NodePool can also be notified when the network is disconnected. +Through the traffic reuse of Pool Scope Data, some nodes in NodePool are disconnected from the cloud network. However, the nodes that are disconnected can also obtain the latest Endpoints/Endpointslices data (because they belong to pool Scope data) through yurt-coordinator, so the service topology changes in NodePool can also be notified when the network is disconnected. ## Implementation History - [ ] 04/14/2022: Present proposal at a community meeting and collect feedbacks. diff --git a/docs/proposals/20220627-yurthub-cache-refactoring.md b/docs/proposals/20220627-yurthub-cache-refactoring.md index 6cf493be9c4..1ff14b2bc83 100644 --- a/docs/proposals/20220627-yurthub-cache-refactoring.md +++ b/docs/proposals/20220627-yurthub-cache-refactoring.md @@ -45,7 +45,7 @@ Refactoring cache-related components in Yurthub. Decouple cache policy with spec ## 2. Motivation -When implementing the feature of nodepool-governance-capability based on Pool-Coordinator, it's found that CacheManager is coupled with DiskStorage, depending deeply on features of file system. Thus, it's difficult to add a new storage of Pool-Coordinator. In addition, the responsibility of each cache-related component is not clear, making it difficult to maintain the code. For example, when querying cache for nodelease object, it will use the in-memory cache to speed up the query. However, the in-memory cache is implemented in the StorageWrapper, rather than in the CacheManager which should manage the cache policy. +When implementing the feature of nodepool-governance-capability based on Yurt-Coordinator, it's found that CacheManager is coupled with DiskStorage, depending deeply on features of file system. Thus, it's difficult to add a new storage of Yurt-Coordinator. In addition, the responsibility of each cache-related component is not clear, making it difficult to maintain the code. For example, when querying cache for nodelease object, it will use the in-memory cache to speed up the query. However, the in-memory cache is implemented in the StorageWrapper, rather than in the CacheManager which should manage the cache policy. ## 3. Problems of Current Cache Structure @@ -62,7 +62,7 @@ In the current implementation, when updating the object in the storage, CacheMan #### 3.1.2 key of object depends on the DiskStorage implementation -Currently, the key of object used in CacheManager is generated through `util.KeyFunc`, in the format of `component/resources/namesapce/name` which can only be recognized by DiskStorage. In Pool-Coordinator, the key format should be `/registry/resources/namespace/name`, otherwise it cannot be recognized by the APIServer. It's obvious that `util.KeyFunc` is not generic for all storages. +Currently, the key of object used in CacheManager is generated through `util.KeyFunc`, in the format of `component/resources/namesapce/name` which can only be recognized by DiskStorage. In Yurt-Coordinator, the key format should be `/registry/resources/namespace/name`, otherwise it cannot be recognized by the APIServer. It's obvious that `util.KeyFunc` is not generic for all storages. #### 3.1.3 storage recycling when deleting cache-agent depends on the DiskStorage implementation @@ -70,7 +70,7 @@ When deleting a cache-agent, CacheManager should recycle the cache used by this #### 3.1.4 the implementation of saving list objects depends on the DiskStorage implementation -As described in [#265](https://github.com/openyurtio/openyurt/pull/265), each cache-agent can only have the cache of one type of list for one resource. Considering that if we update cache using items in list object one by one, it will result in some cache objects not being deleted. Thus, in `saveListObject`, it will replace all objects under the resource directory with the items in the response of the list request. It works well when the CacheManager uses DiskStorage, because cache for different components are stored at different directory, for example, service cache for kubelet is under `/etc/kubernetes/cache/kubelet/services`, service cache for kube-proxy is under `/etc/kubernetes/cache/kube-proxy/services`. Replacing the serivce cache of kubelet has no influence on service cache of kube-proxy. But when using Pool-Coordinator storage, services for all components are cached under `/registry/services`, if replacing all the entries under `/registry/services` with items in the response of list request from kubelet, the service cache for kube-proxy will be overwritten. +As described in [#265](https://github.com/openyurtio/openyurt/pull/265), each cache-agent can only have the cache of one type of list for one resource. Considering that if we update cache using items in list object one by one, it will result in some cache objects not being deleted. Thus, in `saveListObject`, it will replace all objects under the resource directory with the items in the response of the list request. It works well when the CacheManager uses DiskStorage, because cache for different components are stored at different directory, for example, service cache for kubelet is under `/etc/kubernetes/cache/kubelet/services`, service cache for kube-proxy is under `/etc/kubernetes/cache/kube-proxy/services`. Replacing the serivce cache of kubelet has no influence on service cache of kube-proxy. But when using Yurt-Coordinator storage, services for all components are cached under `/registry/services`, if replacing all the entries under `/registry/services` with items in the response of list request from kubelet, the service cache for kube-proxy will be overwritten. ### 3.2 Definition of Store Interface is not explicit @@ -80,13 +80,13 @@ In the implementation of Update of DiskStorage, it will also create the key if i #### 3.2.2 Definition of DeleteCollection is not explicit -Currently, the implementation of DeleteCollection in DiskStorage is just delete the passed-in rootKey as a directory . The DeleteCollection function is used only when recycling cache of cache-agent. If the DeleteCollection is used to delete all keys with the prefix of rootKey, it cannot successfully recycling the cache of the cache-agent in the Pool-Coordinator storage, because the key is not has the component name as its prefix. If the DeleteCollection is used to recycling the cache of cache-agent, its parameter should not be rootKey. Only DiskStorage can recognize it as rootKey. +Currently, the implementation of DeleteCollection in DiskStorage is just delete the passed-in rootKey as a directory . The DeleteCollection function is used only when recycling cache of cache-agent. If the DeleteCollection is used to delete all keys with the prefix of rootKey, it cannot successfully recycling the cache of the cache-agent in the Yurt-Coordinator storage, because the key is not has the component name as its prefix. If the DeleteCollection is used to recycling the cache of cache-agent, its parameter should not be rootKey. Only DiskStorage can recognize it as rootKey. ### 3.3 Responsibility of each cache-related component is not explicit #### 3.3.1 StorageWrapper should not care about in-memory cache -Under the circumstance of edge autonomy, YurtHub will use the in-memory cache to speed up the process of specific requests from edge components, for example nodes request and leases request from kubelet. The implementation of this optimization is in StorageWrapper. Thus the StorageWrapper should know which component the request from. Considering that CacheManager only pass the key value as argument to StorageWrapper and not all key value has the information of component(such as key of Pool-Coordinator), the CacheManager should additionally expose the component details to the StorageWrapper, which will result in the coupling of CacheManager and StorageWrapper, and make both two components have the logic of cache policy. +Under the circumstance of edge autonomy, YurtHub will use the in-memory cache to speed up the process of specific requests from edge components, for example nodes request and leases request from kubelet. The implementation of this optimization is in StorageWrapper. Thus the StorageWrapper should know which component the request from. Considering that CacheManager only pass the key value as argument to StorageWrapper and not all key value has the information of component(such as key of Yurt-Coordinator), the CacheManager should additionally expose the component details to the StorageWrapper, which will result in the coupling of CacheManager and StorageWrapper, and make both two components have the logic of cache policy. #### 3.3.2 CacheManager should not care about the key format @@ -102,7 +102,7 @@ Some non-cache related components also use DiskStorage to read/write file from/t Currently, disk storage in yurthub uses the key format `component/resource/namespace/name`, and support caching resources for components kubelet, kube-proxy, coredns, flannel and yurthub. Users can manually set the cache agent as `*` in configmap `yurt-hub-cfg` to extend the capability to other components. There comes the problem that yurthub currently cannot distinguish resources with same name but in different versions and groups, such as `NetworkPolicy` in `networking.k8s.io` and in `crd.projectcalico.org` which are both used by calico and their keys have the same prefix `go-http-client/networkpolicies`. When calico starts to list/watch NetworkPolicy in `networking.k8s.io`, it may replace the cache of NetworkPolicy in `crd.projectcalico.org` which cause errors. -Thus, we should enable yurthub, to be specific, disk storage, to distinguish resources with same name but different versions and groups. We can change the key format of disk storage as `component/resource.version.group/namespace/name` to solve the problem. And we should only make disk storage to use this key format without influence on the implementation of other storages, such as etcd for pool-coordinator. +Thus, we should enable yurthub, to be specific, disk storage, to distinguish resources with same name but different versions and groups. We can change the key format of disk storage as `component/resource.version.group/namespace/name` to solve the problem. And we should only make disk storage to use this key format without influence on the implementation of other storages, such as etcd for yurt-coordinator. ### 4.2 Avoid Watch Request Flood When Yurthub offline @@ -126,7 +126,7 @@ The **Policy layer** takes the responsibility of cache policy, including determi The **Serialization layer** takes the responsibility of serialization/unserialization of cached objects. The logic in this layer is related to Kubernetes APIMachinery. The byte formats it needs to concern include json, yaml and protobuf. The types of objects it needs to concern include kubernetes native resources and CRDs. Currently, the component in this layer is StorageWrapper. -The **Storage Frontend** layer serves like a shim between the Serialization layer and Stroage Backend layer. It should provide interface to cache objects shielding the differences among different storages for the upper-layer. It also takes the responsibility of implementation of KeyFunc. Currently, the component in this layer is DiskStorage. We can add more storage in this layer later, such as Pool-Coordinator Storage. +The **Storage Frontend** layer serves like a shim between the Serialization layer and Stroage Backend layer. It should provide interface to cache objects shielding the differences among different storages for the upper-layer. It also takes the responsibility of implementation of KeyFunc. Currently, the component in this layer is DiskStorage. We can add more storage in this layer later, such as Yurt-Coordinator Storage. The **Storage Backend layer** is the entity that interacts with the storage to complete the actual storage operation. It can be implemented by ourselves, such as FS Operator, or be provided by third-party, such as clientv3 pkg of etcd. diff --git a/docs/roadmap.md b/docs/roadmap.md index 1cbc0eb1327..8f49814d689 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -112,12 +112,12 @@ detail info: https://github.com/orgs/openyurtio/projects/4 **ControlPlane SIG** - Provide NodePool Governance Capability - - add pool-coordinator-certificate controller ([#774](https://github.com/openyurtio/openyurt/issues/774)) + - add yurt-coordinator-certificate controller ([#774](https://github.com/openyurtio/openyurt/issues/774)) - add admission webhook ([#775](https://github.com/openyurtio/openyurt/issues/775)) - - remove nodelifecycle controller and add pool-coordinator controller in yurt-controller-manager component ([#776](https://github.com/openyurtio/openyurt/issues/776)) - - add pool-coordinator component ([#777](https://github.com/openyurtio/openyurt/issues/777)) + - remove nodelifecycle controller and add yurt-coordinator controller in yurt-controller-manager component ([#776](https://github.com/openyurtio/openyurt/issues/776)) + - add yurt-coordinator component ([#777](https://github.com/openyurtio/openyurt/issues/777)) - yurthub are delegated to report heartbeats for nodes that disconnected with cloud ([#779](https://github.com/openyurtio/openyurt/issues/779)) -- pool-coordinator supports share pool scope data in the nodepool ([#778](https://github.com/openyurtio/openyurt/issues/778)) +- yurt-coordinator supports share pool scope data in the nodepool ([#778](https://github.com/openyurtio/openyurt/issues/778)) - Improve Yurtadm Join command ([#889](https://github.com/openyurtio/openyurt/issues/889)) - Improve Yurtadm Reset command ([#1058](https://github.com/openyurtio/openyurt/issues/1058)) diff --git a/hack/lib/sync-charts.sh b/hack/lib/sync-charts.sh index 74913ba27dc..7a4fad7d250 100644 --- a/hack/lib/sync-charts.sh +++ b/hack/lib/sync-charts.sh @@ -28,14 +28,14 @@ git config --global user.email "openyurt-bot@openyurt.io" git config --global user.name "openyurt-bot" git clone --single-branch --depth 1 git@github.com:openyurtio/openyurt-helm.git openyurt-helm -echo "clear openyurt-helm charts/pool-coordinator" +echo "clear openyurt-helm charts/yurt-coordinator" -if [ -d "openyurt-helm/charts/pool-coordinator" ] +if [ -d "openyurt-helm/charts/yurt-coordinator" ] then - echo "charts pool-coordinator exists, remove it" - rm -r openyurt-helm/charts/pool-coordinator/* + echo "charts yurt-coordinator exists, remove it" + rm -r openyurt-helm/charts/yurt-coordinator/* else - mkdir -p openyurt-helm/charts/pool-coordinator + mkdir -p openyurt-helm/charts/yurt-coordinator fi echo "clear openyurt-helm charts/yurt-manager" @@ -60,7 +60,7 @@ fi echo "copy folder openyurt/charts to openyurt-helm/charts" -cp -R openyurt/charts/pool-coordinator/* openyurt-helm/charts/pool-coordinator/ +cp -R openyurt/charts/yurt-coordinator/* openyurt-helm/charts/yurt-coordinator/ cp -R openyurt/charts/yurt-manager/* openyurt-helm/charts/yurt-manager/ cp -R openyurt/charts/yurthub/* openyurt-helm/charts/yurthub/ From e9dce787526ee198749290bc220c8cd080865be8 Mon Sep 17 00:00:00 2001 From: Liang Deng <283304489@qq.com> Date: Mon, 12 Jun 2023 14:07:11 +0800 Subject: [PATCH 27/93] feat: remove yurtadm init command (#1537) Signed-off-by: Liang Deng <283304489@qq.com> --- pkg/yurtadm/cmd/cmd.go | 2 - pkg/yurtadm/cmd/yurtinit/init.go | 248 ---------------------------- pkg/yurtadm/cmd/yurtinit/options.go | 91 ---------- 3 files changed, 341 deletions(-) delete mode 100644 pkg/yurtadm/cmd/yurtinit/init.go delete mode 100644 pkg/yurtadm/cmd/yurtinit/options.go diff --git a/pkg/yurtadm/cmd/cmd.go b/pkg/yurtadm/cmd/cmd.go index 761310725a6..6c22e4ad99c 100644 --- a/pkg/yurtadm/cmd/cmd.go +++ b/pkg/yurtadm/cmd/cmd.go @@ -31,7 +31,6 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/renew" "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/reset" "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/token" - "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/yurtinit" ) // NewYurtadmCommand creates a new yurtadm command @@ -46,7 +45,6 @@ func NewYurtadmCommand() *cobra.Command { setVersion(cmds) // add kubeconfig to persistent flags cmds.PersistentFlags().String("kubeconfig", "", "The path to the kubeconfig file") - cmds.AddCommand(yurtinit.NewCmdInit()) cmds.AddCommand(join.NewCmdJoin(os.Stdin, os.Stdout, os.Stderr)) cmds.AddCommand(reset.NewCmdReset(os.Stdin, os.Stdout, os.Stderr)) cmds.AddCommand(token.NewCmdToken(os.Stdin, os.Stdout, os.Stderr)) diff --git a/pkg/yurtadm/cmd/yurtinit/init.go b/pkg/yurtadm/cmd/yurtinit/init.go deleted file mode 100644 index dbbd51cbab9..00000000000 --- a/pkg/yurtadm/cmd/yurtinit/init.go +++ /dev/null @@ -1,248 +0,0 @@ -/* -Copyright 2020 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package yurtinit - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "os" - "os/exec" - "runtime" - "sync" - - "github.com/spf13/cobra" - flag "github.com/spf13/pflag" - "k8s.io/klog/v2" - - strutil "github.com/openyurtio/openyurt/pkg/util/strings" - "github.com/openyurtio/openyurt/pkg/yurtadm/constants" - "github.com/openyurtio/openyurt/pkg/yurtadm/util" - "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" -) - -const ( - // APIServerAdvertiseAddress flag sets the IP address the API Server will advertise it's listening on. Specify '0.0.0.0' to use the address of the default network interface. - APIServerAdvertiseAddress = "apiserver-advertise-address" - //YurttunnelServerAddress flag sets the IP address of Yurttunnel Server. - YurttunnelServerAddress = "yurt-tunnel-server-address" - // NetworkingServiceSubnet flag sets the range of IP address for service VIPs. - NetworkingServiceSubnet = "service-cidr" - // NetworkingPodSubnet flag sets the range of IP addresses for the pod network. If set, the control plane will automatically allocate CIDRs for every node. - NetworkingPodSubnet = "pod-network-cidr" - // OpenYurtVersion flag sets the OpenYurt version for the control plane. - OpenYurtVersion = "openyurt-version" - // ImageRepository flag sets the container registry to pull control plane images from. - ImageRepository = "image-repository" - // PassWd flag is the password of master server. - PassWd = "passwd" - - TmpDownloadDir = "/tmp" - - SealerUrlFormat = "https://github.com/alibaba/sealer/releases/download/%s/sealer-%s-linux-%s.tar.gz" - DefaultSealerVersion = "v0.8.6" - - InitClusterImage = "%s/openyurt-cluster:%s" - SealerRunCmd = "sealer run %s/openyurt-cluster:%s -e APIServerAdvertiseAddress=%s,YurttunnelServerAddress=%s,FlannelNetWork=%s,PodSubnet=%s,ServiceSubnet=%s" -) - -var ( - ValidSealerVersions = []string{ - "v0.8.6", - } -) - -// clusterInitializer init a node to master of openyurt cluster -type clusterInitializer struct { - InitOptions -} - -// NewCmdInit use tool sealer to initializer a master of OpenYurt cluster. -// It will deploy all openyurt components, such as yurt-app-manager, yurt-tunnel-server, etc. -func NewCmdInit() *cobra.Command { - o := NewInitOptions() - - cmd := &cobra.Command{ - Use: "init", - Short: "Run this command in order to set up the OpenYurt control plane", - RunE: func(cmd *cobra.Command, args []string) error { - if err := o.Validate(); err != nil { - return err - } - initializer := NewInitializerWithOptions(o) - if err := initializer.Run(); err != nil { - return err - } - return nil - }, - Args: cobra.NoArgs, - } - - addFlags(cmd.Flags(), o) - return cmd -} - -func addFlags(flagset *flag.FlagSet, o *InitOptions) { - flagset.StringVarP( - &o.AdvertiseAddress, APIServerAdvertiseAddress, "", o.AdvertiseAddress, - "The IP address the API Server will advertise it's listening on.", - ) - flagset.StringVarP( - &o.YurttunnelServerAddress, YurttunnelServerAddress, "", o.YurttunnelServerAddress, - "The yurt-tunnel-server address.") - flagset.StringVarP( - &o.ServiceSubnet, NetworkingServiceSubnet, "", o.ServiceSubnet, - "Use alternative range of IP address for service VIPs.", - ) - flagset.StringVarP( - &o.PodSubnet, NetworkingPodSubnet, "", o.PodSubnet, - "Specify range of IP addresses for the pod network. If set, the control plane will automatically allocate CIDRs for every node.", - ) - flagset.StringVarP( - &o.OpenYurtVersion, OpenYurtVersion, "", o.OpenYurtVersion, - `Choose a specific OpenYurt version for the control plane.`, - ) - flagset.StringVarP(&o.ImageRepository, ImageRepository, "", o.ImageRepository, - "Choose a registry to pull cluster images from", - ) -} - -func NewInitializerWithOptions(o *InitOptions) *clusterInitializer { - return &clusterInitializer{ - *o, - } -} - -// Run use sealer to initialize the master node. -func (ci *clusterInitializer) Run() error { - if err := CheckAndInstallSealer(); err != nil { - return err - } - - if err := ci.InstallCluster(); err != nil { - return err - } - return nil -} - -// CheckAndInstallSealer install sealer, skip install if it exists -func CheckAndInstallSealer() error { - klog.Infof("Check and install sealer") - sealerExist := false - if _, err := exec.LookPath("sealer"); err == nil { - if b, err := exec.Command("sealer", "version").CombinedOutput(); err == nil { - info := make(map[string]string) - if err := json.Unmarshal(b, &info); err != nil { - return fmt.Errorf("Can't get the existing sealer version: %w", err) - } - sealerVersion := info["gitVersion"] - if strutil.IsInStringLst(ValidSealerVersions, sealerVersion) { - klog.Infof("Sealer %s already exist, skip install.", sealerVersion) - sealerExist = true - } else { - return fmt.Errorf("The existing sealer version %s is not supported, please clean it. Valid server versions are %v.", sealerVersion, ValidSealerVersions) - } - } - } - - if !sealerExist { - // download and install sealer - packageUrl := fmt.Sprintf(SealerUrlFormat, DefaultSealerVersion, DefaultSealerVersion, runtime.GOARCH) - savePath := fmt.Sprintf("%s/sealer-%s-linux-%s.tar.gz", TmpDownloadDir, DefaultSealerVersion, runtime.GOARCH) - klog.V(1).Infof("Download sealer from: %s", packageUrl) - if err := util.DownloadFile(packageUrl, savePath, 3); err != nil { - return fmt.Errorf("Download sealer fail: %w", err) - } - if err := util.Untar(savePath, TmpDownloadDir); err != nil { - return err - } - comp := "sealer" - target := fmt.Sprintf("/usr/bin/%s", comp) - if err := edgenode.CopyFile(TmpDownloadDir+"/"+comp, target, constants.DirMode); err != nil { - return err - } - } - return nil -} - -// InstallCluster initialize the master of openyurt cluster by calling sealer -func (ci *clusterInitializer) InstallCluster() error { - klog.Infof("init an openyurt cluster") - runCmd := fmt.Sprintf(SealerRunCmd, ci.ImageRepository, ci.OpenYurtVersion, ci.AdvertiseAddress, ci.YurttunnelServerAddress, ci.PodSubnet, ci.PodSubnet, ci.ServiceSubnet) - cmd := exec.Command("bash", "-c", runCmd) - return execCmd(cmd) -} - -// execCmd will execute command and get the real-time output of the screen -func execCmd(cmd *exec.Cmd) error { - cmd.Stdin = os.Stdin - - var wg sync.WaitGroup - wg.Add(2) - - // capture standard output - stdout, err := cmd.StdoutPipe() - if err != nil { - fmt.Println("ERROR:", err) - return err - } - - readout := bufio.NewReader(stdout) - go func() { - defer wg.Done() - getOutput(readout) - }() - - // capture standard error - stderr, err := cmd.StderrPipe() - if err != nil { - fmt.Println("ERROR:", err) - return err - } - - readerr := bufio.NewReader(stderr) - go func() { - defer wg.Done() - getOutput(readerr) - }() - - // run command - cmd.Run() - wg.Wait() - return nil -} - -func getOutput(reader *bufio.Reader) { - var sumOutput string // all the output content of the screen - outputBytes := make([]byte, 200) - for { - // Get the real-time output of the screen - n, err := reader.Read(outputBytes) - if err != nil { - if err == io.EOF { - break - } - fmt.Println(err) - sumOutput += err.Error() - } - output := string(outputBytes[:n]) - fmt.Print(output) - sumOutput += output - } - return -} diff --git a/pkg/yurtadm/cmd/yurtinit/options.go b/pkg/yurtadm/cmd/yurtinit/options.go deleted file mode 100644 index fb81b7e39f8..00000000000 --- a/pkg/yurtadm/cmd/yurtinit/options.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2020 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package yurtinit - -import ( - "net" - - "github.com/pkg/errors" - - "github.com/openyurtio/openyurt/pkg/yurtadm/constants" -) - -// InitOptions defines all the init options exposed via flags by yurtadm init. -type InitOptions struct { - AdvertiseAddress string - YurttunnelServerAddress string - ServiceSubnet string - PodSubnet string - ImageRepository string - OpenYurtVersion string -} - -func NewInitOptions() *InitOptions { - return &InitOptions{ - ImageRepository: constants.DefaultOpenYurtImageRegistry, - OpenYurtVersion: constants.DefaultOpenYurtVersion, - } -} - -func (o *InitOptions) Validate() error { - if err := validateServerAddress(o.AdvertiseAddress); err != nil { - return err - } - - if o.YurttunnelServerAddress != "" { - if err := validateServerAddress(o.YurttunnelServerAddress); err != nil { - return err - } - } - - if o.PodSubnet == "" { - return errors.Errorf("podSubnet can't be empty, you must specify --pod-network-cidr") - } else { - if err := validateCidrString(o.PodSubnet); err != nil { - return err - } - } - - if o.ServiceSubnet == "" { - return errors.Errorf("serviceSubnet can't be empty, you must specify --service-cidr") - } else { - if err := validateCidrString(o.ServiceSubnet); err != nil { - return err - } - } - - return nil -} - -func validateServerAddress(address string) error { - ip := net.ParseIP(address) - if ip == nil { - return errors.Errorf("cannot parse IP address: %s", address) - } - if !ip.IsGlobalUnicast() { - return errors.Errorf("cannot use %q as the bind address for the API Server", address) - } - return nil -} - -func validateCidrString(cidr string) error { - _, _, err := net.ParseCIDR(cidr) - if err != nil { - return nil - } - return nil -} From 70e53a9a3a102ed237c0cbd1ed936534f62277d3 Mon Sep 17 00:00:00 2001 From: y-ykcir Date: Tue, 13 Jun 2023 14:08:11 +0800 Subject: [PATCH 28/93] feat: add e2e test case for yurtstaticset controller (#1518) * feat: add e2e test case for yurtstaticset controller Signed-off-by: ricky * add case for no worker required Signed-off-by: ricky --------- Signed-off-by: ricky --- test/e2e/yurt/yurtstaticset.go | 384 +++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 test/e2e/yurt/yurtstaticset.go diff --git a/test/e2e/yurt/yurtstaticset.go b/test/e2e/yurt/yurtstaticset.go new file mode 100644 index 00000000000..3fe7803978c --- /dev/null +++ b/test/e2e/yurt/yurtstaticset.go @@ -0,0 +1,384 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package yurt + +import ( + "context" + "fmt" + "os/exec" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + "github.com/openyurtio/openyurt/test/e2e/util" + ycfg "github.com/openyurtio/openyurt/test/e2e/yurtconfig" +) + +const ( + staticPodPath string = "/etc/kubernetes/manifests" +) + +var _ = Describe("yurtStaticSet Test", Ordered, func() { + ctx := context.Background() + timeout := 60 * time.Second + k8sClient := ycfg.YurtE2eCfg.RuntimeClient + nodeToImageMap := make(map[string]string) + + var updateStrategyType string + var namespaceName string + + yurtStaticSetName := "busybox" + podName := "busybox" + testContainerName := "bb" + testImg1 := "busybox" + testImg2 := "busybox:1.36.0" + + createNamespace := func() { + ns := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + }, + } + Eventually( + func() error { + return k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground)) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) + By("make sure all the resources are removed") + + res := &corev1.Namespace{} + Eventually( + func() error { + return k8sClient.Get(ctx, client.ObjectKey{ + Name: namespaceName, + }, res) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(&util.NotFoundMatcher{}) + Eventually( + func() error { + return k8sClient.Create(ctx, &ns) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + } + + createStaticPod := func(nodeName string) { + staticPodStr := fmt.Sprintf(` +apiVersion: v1 +kind: Pod +metadata: + name: %s + namespace: %s + labels: + app: %s +spec: + containers: + - name: %s + image: %s + command: + - "/bin/sh" + args: + - "-c" + - "while true; do echo hello; sleep 10; done" +`, podName, namespaceName, podName, testContainerName, testImg1) + cmd := fmt.Sprintf("cat << EOF > %s/%s.yaml%sEOF", staticPodPath, podName, staticPodStr) + dockerCmd := "docker exec -t " + nodeName + " /bin/bash -c " + "'" + cmd + "'" + + _, err := exec.Command("/bin/bash", "-c", dockerCmd).CombinedOutput() + Expect(err).NotTo(HaveOccurred(), "fail to create static pod") + } + + deleteStaticPod := func(nodeName string) { + cmd := fmt.Sprintf("rm -f %s/%s.yaml", staticPodPath, podName) + dockerCmd := "docker exec -t " + nodeName + " /bin/bash -c \"" + cmd + "\"" + + _, err := exec.Command("/bin/bash", "-c", dockerCmd).CombinedOutput() + Expect(err).NotTo(HaveOccurred(), "fail to delete static pod") + } + + createYurtStaticSet := func() { + Eventually(func() error { + return k8sClient.Delete(ctx, &v1alpha1.YurtStaticSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: yurtStaticSetName, + Namespace: namespaceName, + }, + }) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 300).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) + + testLabel := map[string]string{"app": podName} + + testYurtStaticSet := &v1alpha1.YurtStaticSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: yurtStaticSetName, + Namespace: namespaceName, + }, + Spec: v1alpha1.YurtStaticSetSpec{ + StaticPodManifest: podName, + UpgradeStrategy: v1alpha1.YurtStaticSetUpgradeStrategy{ + Type: v1alpha1.YurtStaticSetUpgradeStrategyType(updateStrategyType), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: namespaceName, + Labels: testLabel, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: testContainerName, + Image: testImg1, + Command: []string{"/bin/sh"}, + Args: []string{"-c", "while true; do echo hello; sleep 10; done"}, + }, + }, + }, + }, + }, + } + + Eventually(func() error { + return k8sClient.Create(ctx, testYurtStaticSet) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + } + + updateYurtStaticSet := func() { + Eventually(func() error { + testYurtStaticSet := &v1alpha1.YurtStaticSet{} + if err := k8sClient.Get(ctx, client.ObjectKey{ + Name: yurtStaticSetName, + Namespace: namespaceName, + }, testYurtStaticSet); err != nil { + return err + } + testYurtStaticSet.Spec.Template.Spec.Containers[0].Image = testImg2 + return k8sClient.Update(ctx, testYurtStaticSet) + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(SatisfyAny(BeNil())) + } + + checkPodStatusAndUpdate := func() { + nodeToImageMap = map[string]string{} + Eventually(func() error { + testPods := &corev1.PodList{} + if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"app": podName}); err != nil { + return err + } + if len(testPods.Items) != 2 { + return fmt.Errorf("not reconcile") + } + for _, pod := range testPods.Items { + if pod.Status.Phase != corev1.PodRunning { + return fmt.Errorf("not running") + } + nodeToImageMap[pod.Spec.NodeName] = pod.Spec.Containers[0].Image + } + return nil + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(SatisfyAny(BeNil())) + } + + checkNodeStatus := func(nodeName string) error { + node := &corev1.Node{} + if err := k8sClient.Get(ctx, client.ObjectKey{Name: nodeName}, node); err != nil { + return err + } + for _, condition := range node.Status.Conditions { + if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue { + return nil + } + } + return fmt.Errorf("node openyurt-e2e-test-worker2 is not ready") + } + + reconnectNode := func(nodeName string) { + // reconnect node + cmd := exec.Command("/bin/bash", "-c", "docker network connect kind "+nodeName) + err := cmd.Run() + Expect(err).NotTo(HaveOccurred(), "fail to reconnect "+nodeName+" node to kind bridge") + + Eventually(func() error { + return checkNodeStatus(nodeName) + }).WithTimeout(120 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) + + // restart flannel pod on node to recover flannel NIC + Eventually(func() error { + flannelPods := &corev1.PodList{} + if err := k8sClient.List(ctx, flannelPods, client.InNamespace(FlannelNamespace)); err != nil { + return err + } + if len(flannelPods.Items) != 3 { + return fmt.Errorf("not reconcile") + } + for _, pod := range flannelPods.Items { + if pod.Spec.NodeName == nodeName { + if err := k8sClient.Delete(ctx, &pod); err != nil { + return err + } + } + } + return nil + }).WithTimeout(timeout).Should(SatisfyAny(BeNil())) + } + + BeforeEach(func() { + By("Start to run yurtStaticSet test, clean up previous resources") + nodeToImageMap = map[string]string{} + k8sClient = ycfg.YurtE2eCfg.RuntimeClient + namespaceName = "yurtstaticset-e2e-test" + "-" + rand.String(4) + createNamespace() + }) + + AfterEach(func() { + By("Cleanup resources after test") + deleteStaticPod("openyurt-e2e-test-worker") + deleteStaticPod("openyurt-e2e-test-worker2") + + By(fmt.Sprintf("Delete the entire namespaceName %s", namespaceName)) + Expect(k8sClient.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}}, client.PropagationPolicy(metav1.DeletePropagationBackground))).Should(BeNil()) + }) + + Describe("Test YurtStaticSet AdvancedRollingUpdate upgrade model", func() { + It("Test one worker disconnect", func() { + By("Run staticpod AdvancedRollingUpdate upgrade model test") + // disconnect openyurt-e2e-test-worker2 node + cmd := exec.Command("/bin/bash", "-c", "docker network disconnect kind openyurt-e2e-test-worker2") + err := cmd.Run() + Expect(err).NotTo(HaveOccurred(), "fail to disconnect openyurt-e2e-test-worker2 node to kind bridge: docker network disconnect kind %s") + Eventually(func() error { + return checkNodeStatus("openyurt-e2e-test-worker2") + }).WithTimeout(120 * time.Second).WithPolling(1 * time.Second).Should(SatisfyAll(HaveOccurred(), Not(&util.NotFoundMatcher{}))) + + // update the yurtStaticSet + updateYurtStaticSet() + + // check image version + Eventually(func() error { + checkPodStatusAndUpdate() + if nodeToImageMap["openyurt-e2e-test-worker"] == testImg2 && nodeToImageMap["openyurt-e2e-test-worker2"] == testImg1 { + return nil + } + return fmt.Errorf("error image update") + }).WithTimeout(timeout * 2).WithPolling(time.Millisecond * 1000).Should(Succeed()) + + // recover network environment + reconnectNode("openyurt-e2e-test-worker2") + + // check image version + Eventually(func() error { + checkPodStatusAndUpdate() + if nodeToImageMap["openyurt-e2e-test-worker"] == testImg2 && nodeToImageMap["openyurt-e2e-test-worker2"] == testImg2 { + return nil + } + return fmt.Errorf("error image update") + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(Succeed()) + }) + + It("Testing situation where upgrade is not required", func() { + Consistently(func() error { + podList := &corev1.PodList{} + if err := k8sClient.List(ctx, podList, client.InNamespace(namespaceName)); err != nil { + return err + } + if len(podList.Items) != 2 { + return fmt.Errorf("should no worker pod be created") + } + return nil + }, 10*time.Second, 500*time.Millisecond).Should(Succeed()) + }) + + BeforeEach(func() { + By("Prepare for staticpod AdvancedRollingUpdate upgrade model test") + updateStrategyType = "AdvancedRollingUpdate" + + createStaticPod("openyurt-e2e-test-worker") + createStaticPod("openyurt-e2e-test-worker2") + + checkPodStatusAndUpdate() + createYurtStaticSet() + }) + + AfterEach(func() { + By("Reconnect openyurt-e2e-test-worker2 node if it is disconnected") + if err := checkNodeStatus("openyurt-e2e-test-worker2"); err == nil { + return + } + // reconnect openyurt-e2e-test-worker2 node to avoid impact on other tests + reconnectNode("openyurt-e2e-test-worker2") + }) + }) + + Describe("Test YurtStaticSet ota upgrade model", func() { + It("Test ota update for one worker", func() { + By("Run staticpod ota upgrade model test") + var pN2 string + updateStrategyType = "OTA" + + createStaticPod("openyurt-e2e-test-worker") + createStaticPod("openyurt-e2e-test-worker2") + + createYurtStaticSet() + checkPodStatusAndUpdate() + + // update the yurtstaticset + updateYurtStaticSet() + + // check status condition PodNeedUpgrade + Eventually(func() error { + testPods := &corev1.PodList{} + if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"app": podName}); err != nil { + return err + } + if len(testPods.Items) != 2 { + return fmt.Errorf("not reconcile") + } + for _, pod := range testPods.Items { + for _, condition := range pod.Status.Conditions { + if condition.Type == PodNeedUpgrade && condition.Status != corev1.ConditionTrue { + return fmt.Errorf("pod %s status condition PodNeedUpgrade is not true", pod.Name) + } + } + if pod.Spec.NodeName == "openyurt-e2e-test-worker2" { + pN2 = pod.Name + } + } + return nil + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(SatisfyAny(BeNil())) + + // ota update for openyurt-e2e-test-worker2 node + Eventually(func() string { + curlCmd := fmt.Sprintf("curl -X POST %s:%s/openyurt.io/v1/namespaces/%s/pods/%s/upgrade", ServerName, ServerPort, namespaceName, pN2) + opBytes, err := exec.Command("/bin/bash", "-c", "docker exec -t openyurt-e2e-test-worker2 /bin/bash -c '"+curlCmd+"'").CombinedOutput() + + if err != nil { + return "" + } + return string(opBytes) + }).WithTimeout(10*time.Second).WithPolling(1*time.Second).Should(ContainSubstring("Start updating pod"), "fail to ota update for pod") + + // check image version + Eventually(func() error { + checkPodStatusAndUpdate() + if nodeToImageMap["openyurt-e2e-test-worker"] == testImg1 && nodeToImageMap["openyurt-e2e-test-worker2"] == testImg2 { + return nil + } + return fmt.Errorf("error image update") + }).WithTimeout(timeout).WithPolling(time.Millisecond * 500).Should(Succeed()) + }) + }) +}) From 731b71e7e9487eb8c57d9f242db33f9e5c330cda Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Tue, 13 Jun 2023 14:13:29 +0800 Subject: [PATCH 29/93] add yurtadm join node in specified nodepool (#1402) * add yurtadm join node in specified nodepool --- .../yurthub-cloud-yurtstaticset.yaml | 3 + .../templates/yurthub-yurtstaticset.yaml | 3 + pkg/util/kubeconfig/kubeconfig.go | 17 +++ pkg/util/kubeconfig/kubeconfig_test.go | 2 + .../kubeadm/app/util/apiclient/idempotency.go | 23 ++++ pkg/yurtadm/cmd/join/join.go | 25 ++++ pkg/yurtadm/cmd/join/join_test.go | 12 +- pkg/yurtadm/cmd/join/joindata/data.go | 1 + pkg/yurtadm/constants/constants.go | 5 + pkg/yurtadm/util/yurthub/yurthub.go | 3 + pkg/yurtadm/util/yurthub/yurthub_test.go | 109 ++++++++++++++++++ 11 files changed, 202 insertions(+), 1 deletion(-) diff --git a/charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml b/charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml index a287fac1aa0..b65890a1075 100644 --- a/charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml +++ b/charts/yurthub/templates/yurthub-cloud-yurtstaticset.yaml @@ -42,6 +42,9 @@ spec: {{- if .Values.organizations }} - --hub-cert-organizations={{ .Values.organizations }} {{- end }} + {{- if .nodePoolName }} + - --nodepool-name={{ .Values.nodePoolName }} + {{- end }} livenessProbe: httpGet: host: {{ .Values.yurthubBindingAddr }} diff --git a/charts/yurthub/templates/yurthub-yurtstaticset.yaml b/charts/yurthub/templates/yurthub-yurtstaticset.yaml index 172dc91576e..7b58e770e8a 100644 --- a/charts/yurthub/templates/yurthub-yurtstaticset.yaml +++ b/charts/yurthub/templates/yurthub-yurtstaticset.yaml @@ -42,6 +42,9 @@ spec: {{- if .Values.organizations }} - --hub-cert-organizations={{ .Values.organizations }} {{- end }} + {{- if .nodePoolName }} + - --nodepool-name={{ .Values.nodePoolName }} + {{- end }} livenessProbe: httpGet: host: {{ .Values.yurthubBindingAddr }} diff --git a/pkg/util/kubeconfig/kubeconfig.go b/pkg/util/kubeconfig/kubeconfig.go index 9625d2d3351..ca713622390 100644 --- a/pkg/util/kubeconfig/kubeconfig.go +++ b/pkg/util/kubeconfig/kubeconfig.go @@ -24,6 +24,8 @@ import ( clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + yurtclientset "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned" ) // CreateBasic creates a basic, general KubeConfig object that then can be extended @@ -125,3 +127,18 @@ func GetAuthInfoFromKubeConfig(config *clientcmdapi.Config) *clientcmdapi.AuthIn } return nil } + +// ToYurtClientSet converts a KubeConfig object to a yurtClient +func ToYurtClientSet(config *clientcmdapi.Config) (yurtclientset.Interface, error) { + overrides := clientcmd.ConfigOverrides{Timeout: "10s"} + clientConfig, err := clientcmd.NewDefaultClientConfig(*config, &overrides).ClientConfig() + if err != nil { + return nil, errors.Wrap(err, "failed to create yurt client configuration from kubeconfig") + } + + client, err := yurtclientset.NewForConfig(clientConfig) + if err != nil { + return nil, errors.Wrap(err, "failed to create yurt client") + } + return client, nil +} diff --git a/pkg/util/kubeconfig/kubeconfig_test.go b/pkg/util/kubeconfig/kubeconfig_test.go index b1cce66144a..11d1a075e65 100644 --- a/pkg/util/kubeconfig/kubeconfig_test.go +++ b/pkg/util/kubeconfig/kubeconfig_test.go @@ -183,6 +183,8 @@ func TestWriteKubeconfigToDisk(t *testing.T) { newFile, ) } + client, err := ToYurtClientSet(c) + t.Log(client, err) }) } } diff --git a/pkg/util/kubernetes/kubeadm/app/util/apiclient/idempotency.go b/pkg/util/kubernetes/kubeadm/app/util/apiclient/idempotency.go index cf576c708b0..e8d85e2372e 100644 --- a/pkg/util/kubernetes/kubeadm/app/util/apiclient/idempotency.go +++ b/pkg/util/kubernetes/kubeadm/app/util/apiclient/idempotency.go @@ -29,6 +29,8 @@ import ( clientsetretry "k8s.io/client-go/util/retry" "github.com/openyurtio/openyurt/pkg/util/kubernetes/kubeadm/app/constants" + nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" + yurtclientset "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned" ) // ConfigMapMutator is a function that mutates the given ConfigMap and optionally returns an error @@ -131,3 +133,24 @@ func GetConfigMapWithRetry(client clientset.Interface, namespace, name string) ( } return nil, lastError } + +func GetNodePoolInfoWithRetry(client yurtclientset.Interface, name string) (*nodepoolv1alpha1.NodePool, error) { + var np *nodepoolv1alpha1.NodePool + var lastError error + err := wait.ExponentialBackoff(clientsetretry.DefaultBackoff, func() (bool, error) { + var err error + np, err = client.AppsV1alpha1().NodePools().Get(context.TODO(), name, metav1.GetOptions{}) + if err == nil { + return true, nil + } + if apierrors.IsNotFound(err) { + return true, nil + } + lastError = err + return false, nil + }) + if err == nil { + return np, nil + } + return nil, lastError +} diff --git a/pkg/yurtadm/cmd/join/join.go b/pkg/yurtadm/cmd/join/join.go index 3edde32b5f4..05e711914ec 100644 --- a/pkg/yurtadm/cmd/join/join.go +++ b/pkg/yurtadm/cmd/join/join.go @@ -32,17 +32,20 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" kubeconfigutil "github.com/openyurtio/openyurt/pkg/util/kubeconfig" + "github.com/openyurtio/openyurt/pkg/util/kubernetes/kubeadm/app/util/apiclient" "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/join/joindata" yurtphases "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/join/phases" yurtconstants "github.com/openyurtio/openyurt/pkg/yurtadm/constants" "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" + nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" ) type joinOptions struct { token string nodeType string nodeName string + nodePoolName string criSocket string organizations string pauseImage string @@ -125,6 +128,10 @@ func addJoinConfigFlags(flagSet *flag.FlagSet, joinOptions *joinOptions) { &joinOptions.namespace, yurtconstants.Namespace, joinOptions.namespace, `Specify the namespace of the yurthub staticpod configmap, if not specified, the namespace will be default.`, ) + flagSet.StringVar( + &joinOptions.nodePoolName, yurtconstants.NodePoolName, joinOptions.nodePoolName, + `Specify the nodePool name. if specified, that will add node into specified nodePool.`, + ) flagSet.StringVar( &joinOptions.criSocket, yurtconstants.NodeCRISocket, joinOptions.criSocket, "Path to the CRI socket to connect", @@ -276,6 +283,7 @@ func newJoinData(args []string, opt *joinOptions) (*joinData, error) { nodeLabels: make(map[string]string), joinNodeData: &joindata.NodeRegistration{ Name: name, + NodePoolName: opt.nodePoolName, WorkingMode: opt.nodeType, CRISocket: opt.criSocket, Organizations: opt.organizations, @@ -320,6 +328,23 @@ func newJoinData(args []string, opt *joinOptions) (*joinData, error) { return nil, err } data.kubernetesVersion = k8sVersion + + // check whether specified nodePool exists + if len(opt.nodePoolName) != 0 { + yurtClient, err := kubeconfigutil.ToYurtClientSet(cfg) + if err != nil { + klog.Errorf("failed to create yurt client, %v", err) + return nil, err + } + + np, err := apiclient.GetNodePoolInfoWithRetry(yurtClient, opt.nodePoolName) + if err != nil || np == nil { + // the specified nodePool not exist, return + return nil, errors.Errorf("when --nodepool-name is specified, the specified nodePool should be exist.") + } + // add nodePool label for node by kubelet + data.nodeLabels[nodepoolv1alpha1.LabelDesiredNodePool] = opt.nodePoolName + } klog.Infof("node join data info: %#+v", *data) // get the yurthub template from the staticpod cr diff --git a/pkg/yurtadm/cmd/join/join_test.go b/pkg/yurtadm/cmd/join/join_test.go index 0d486d536db..0536d163607 100644 --- a/pkg/yurtadm/cmd/join/join_test.go +++ b/pkg/yurtadm/cmd/join/join_test.go @@ -235,6 +235,10 @@ func TestRun(t *testing.T) { func TestNewJoinData(t *testing.T) { jo := newJoinOptions() + jo2 := newJoinOptions() + jo2.token = "v22u0b.17490yh3xp8azpr0" + jo2.unsafeSkipCAVerification = true + jo2.nodePoolName = "nodePool2" tests := []struct { name string @@ -244,10 +248,16 @@ func TestNewJoinData(t *testing.T) { }{ { "normal", - []string{}, + []string{"localhost:8080"}, jo, nil, }, + { + "norma2", + []string{"localhost:8080"}, + jo2, + nil, + }, } for _, tt := range tests { diff --git a/pkg/yurtadm/cmd/join/joindata/data.go b/pkg/yurtadm/cmd/join/joindata/data.go index a7610a7d7ad..9d50ccdd570 100644 --- a/pkg/yurtadm/cmd/join/joindata/data.go +++ b/pkg/yurtadm/cmd/join/joindata/data.go @@ -24,6 +24,7 @@ import ( type NodeRegistration struct { Name string + NodePoolName string CRISocket string WorkingMode string Organizations string diff --git a/pkg/yurtadm/constants/constants.go b/pkg/yurtadm/constants/constants.go index 780eeae57d4..49eded67302 100644 --- a/pkg/yurtadm/constants/constants.go +++ b/pkg/yurtadm/constants/constants.go @@ -84,6 +84,8 @@ const ( NodeLabels = "node-labels" // NodeName flag sets the node name. NodeName = "node-name" + // NodePoolName flag sets the nodePool name. + NodePoolName = "nodepool-name" // NodeType flag sets the type of worker node to edge or cloud. NodeType = "node-type" // Organizations flag sets the extra organizations of hub agent client certificate. @@ -234,6 +236,9 @@ spec: {{if .organizations }} - --hub-cert-organizations={{.organizations}} {{end}} + {{if .nodePoolName }} + - --nodepool-name={{.nodePoolName}} + {{end}} livenessProbe: httpGet: host: {{.yurthubBindingAddr}} diff --git a/pkg/yurtadm/util/yurthub/yurthub.go b/pkg/yurtadm/util/yurthub/yurthub.go index 4671bb6a179..8dacfc4fd76 100644 --- a/pkg/yurtadm/util/yurthub/yurthub.go +++ b/pkg/yurtadm/util/yurthub/yurthub.go @@ -70,6 +70,9 @@ func AddYurthubStaticYaml(data joindata.YurtJoinData, podManifestPath string) er "namespace": data.Namespace(), "image": data.YurtHubImage(), } + if len(data.NodeRegistration().NodePoolName) != 0 { + ctx["nodePoolName"] = data.NodeRegistration().NodePoolName + } yurthubTemplate, err := templates.SubsituteTemplate(data.YurtHubTemplate(), ctx) if err != nil { diff --git a/pkg/yurtadm/util/yurthub/yurthub_test.go b/pkg/yurtadm/util/yurthub/yurthub_test.go index 78c005e805d..0488df46ed3 100644 --- a/pkg/yurtadm/util/yurthub/yurthub_test.go +++ b/pkg/yurtadm/util/yurthub/yurthub_test.go @@ -20,6 +20,11 @@ import ( "testing" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/sets" + clientset "k8s.io/client-go/kubernetes" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/join/joindata" ) var ( @@ -225,3 +230,107 @@ func Test_useRealServerAddr(t *testing.T) { }) } } + +type testData struct { + joinNodeData *joindata.NodeRegistration +} + +func (j *testData) ServerAddr() string { + return "" +} + +func (j *testData) JoinToken() string { + return "" +} + +func (j *testData) PauseImage() string { + return "" +} + +func (j *testData) YurtHubImage() string { + return "" +} + +func (j *testData) YurtHubServer() string { + return "" +} + +func (j *testData) YurtHubTemplate() string { + return "" +} + +func (j *testData) YurtHubManifest() string { + return "" +} + +func (j *testData) KubernetesVersion() string { + return "" +} + +func (j *testData) TLSBootstrapCfg() *clientcmdapi.Config { + return nil +} + +func (j *testData) BootstrapClient() *clientset.Clientset { + return nil +} + +func (j *testData) NodeRegistration() *joindata.NodeRegistration { + return j.joinNodeData +} + +func (j *testData) IgnorePreflightErrors() sets.String { + return nil +} + +func (j *testData) CaCertHashes() []string { + return nil +} + +func (j *testData) NodeLabels() map[string]string { + return nil +} + +func (j *testData) KubernetesResourceServer() string { + return "" +} + +func (j *testData) ReuseCNIBin() bool { + return false +} + +func (j *testData) Namespace() string { + return "" +} + +func TestAddYurthubStaticYaml(t *testing.T) { + xdata := testData{ + joinNodeData: &joindata.NodeRegistration{ + Name: "name1", + NodePoolName: "nodePool1", + CRISocket: "", + WorkingMode: "edge", + Organizations: "", + }} + + tests := []struct { + name string + data testData + podManifestPath string + wantErr bool + }{ + { + name: "test", + data: xdata, + podManifestPath: "/tmp", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := AddYurthubStaticYaml(&tt.data, tt.podManifestPath); (err != nil) != tt.wantErr { + t.Errorf("AddYurthubStaticYaml() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From c3cf90baf2d0fcb68eca777394219b6e943ab793 Mon Sep 17 00:00:00 2001 From: vie-serendipity <60083692+vie-serendipity@users.noreply.github.com> Date: Tue, 13 Jun 2023 14:14:23 +0800 Subject: [PATCH 30/93] check nil in podbinding_controller.go, and handle some errors and conflicts (#1538) --- .../podbinding/podbinding_controller.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go b/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go index 8089cc39285..64a702e0650 100644 --- a/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go +++ b/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go @@ -114,10 +114,14 @@ func (r *ReconcilePodBinding) processNode(ctx context.Context, node *corev1.Node // pod binding takes precedence against node autonomy if isPodBoundenToNode(node) { - r.configureTolerationForPod(pod, nil) + if err := r.configureTolerationForPod(pod, nil); err != nil { + klog.Errorf("failed to configure toleration of pod, %v", err) + } } else { tolerationSeconds := int64(defaultTolerationSeconds) - r.configureTolerationForPod(pod, &tolerationSeconds) + if err := r.configureTolerationForPod(pod, &tolerationSeconds); err != nil { + klog.Errorf("failed to configure toleration of pod, %v", err) + } } } } @@ -139,7 +143,11 @@ func (r *ReconcilePodBinding) configureTolerationForPod(pod *corev1.Pod, tolerat toleratesNodeUnreachable := addOrUpdateTolerationInPodSpec(&pod.Spec, &unreachableToleration) if toleratesNodeNotReady || toleratesNodeUnreachable { - klog.V(4).Infof("pod(%s/%s) => toleratesNodeNotReady=%v, toleratesNodeUnreachable=%v, tolerationSeconds=%d", pod.Namespace, pod.Name, toleratesNodeNotReady, toleratesNodeUnreachable, *tolerationSeconds) + if tolerationSeconds == nil { + klog.V(4).Infof("pod(%s/%s) => toleratesNodeNotReady=%v, toleratesNodeUnreachable=%v, tolerationSeconds=0", pod.Namespace, pod.Name, toleratesNodeNotReady, toleratesNodeUnreachable) + } else { + klog.V(4).Infof("pod(%s/%s) => toleratesNodeNotReady=%v, toleratesNodeUnreachable=%v, tolerationSeconds=%d", pod.Namespace, pod.Name, toleratesNodeNotReady, toleratesNodeUnreachable, *tolerationSeconds) + } _, err := r.podBindingClient.CoreV1().Pods(pod.Namespace).Update(context.TODO(), pod, metav1.UpdateOptions{}) if err != nil { klog.Errorf("failed to update toleration of pod(%s/%s), %v", pod.Namespace, pod.Name, err) @@ -156,12 +164,12 @@ func (r *ReconcilePodBinding) InjectClient(c client.Client) error { } func (r *ReconcilePodBinding) InjectConfig(cfg *rest.Config) error { - client, err := kubernetes.NewForConfig(cfg) + clientSet, err := kubernetes.NewForConfig(cfg) if err != nil { klog.Errorf("failed to create kube client, %v", err) return err } - r.podBindingClient = client + r.podBindingClient = clientSet return nil } From 94243d5caf11b112fd52b3a240dddd7181224901 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Sun, 18 Jun 2023 20:29:12 +0800 Subject: [PATCH 31/93] rename pool-coordinator to yurt-coordinator for charts (#1551) --- charts/{pool-coordinator => yurt-coordinator}/.helmignore | 0 charts/{pool-coordinator => yurt-coordinator}/Chart.yaml | 0 .../{pool-coordinator => yurt-coordinator}/templates/_helpers.tpl | 0 .../templates/yurt-coordinator.yaml} | 0 charts/{pool-coordinator => yurt-coordinator}/values.yaml | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename charts/{pool-coordinator => yurt-coordinator}/.helmignore (100%) rename charts/{pool-coordinator => yurt-coordinator}/Chart.yaml (100%) rename charts/{pool-coordinator => yurt-coordinator}/templates/_helpers.tpl (100%) rename charts/{pool-coordinator/templates/pool-coordinator.yaml => yurt-coordinator/templates/yurt-coordinator.yaml} (100%) rename charts/{pool-coordinator => yurt-coordinator}/values.yaml (100%) diff --git a/charts/pool-coordinator/.helmignore b/charts/yurt-coordinator/.helmignore similarity index 100% rename from charts/pool-coordinator/.helmignore rename to charts/yurt-coordinator/.helmignore diff --git a/charts/pool-coordinator/Chart.yaml b/charts/yurt-coordinator/Chart.yaml similarity index 100% rename from charts/pool-coordinator/Chart.yaml rename to charts/yurt-coordinator/Chart.yaml diff --git a/charts/pool-coordinator/templates/_helpers.tpl b/charts/yurt-coordinator/templates/_helpers.tpl similarity index 100% rename from charts/pool-coordinator/templates/_helpers.tpl rename to charts/yurt-coordinator/templates/_helpers.tpl diff --git a/charts/pool-coordinator/templates/pool-coordinator.yaml b/charts/yurt-coordinator/templates/yurt-coordinator.yaml similarity index 100% rename from charts/pool-coordinator/templates/pool-coordinator.yaml rename to charts/yurt-coordinator/templates/yurt-coordinator.yaml diff --git a/charts/pool-coordinator/values.yaml b/charts/yurt-coordinator/values.yaml similarity index 100% rename from charts/pool-coordinator/values.yaml rename to charts/yurt-coordinator/values.yaml From 16d251df30f1906c675fd1df86e118999b293fe5 Mon Sep 17 00:00:00 2001 From: rambohe Date: Sun, 18 Jun 2023 21:56:12 +0800 Subject: [PATCH 32/93] remove watch nodepool in service topology controller (#1554) --- .../servicetopology/adapter/adapter.go | 2 - .../adapter/endpoints_adapter.go | 36 --------- .../adapter/endpoints_adapter_test.go | 68 ----------------- .../adapter/endpointslicev1_adapter.go | 35 --------- .../adapter/endpointslicev1_adapter_test.go | 68 ----------------- .../adapter/endpointslicev1beta1_adapter.go | 36 --------- .../endpointslicev1beta1_adapter_test.go | 73 ------------------ .../endpoints/endpoints_controller.go | 25 ++----- .../endpoints/endpoints_enqueue_handlers.go | 74 ------------------- .../endpointslice/endpointslice_controller.go | 9 --- .../endpointslice_enqueue_handlers.go | 74 ------------------- pkg/controller/servicetopology/util/util.go | 31 -------- 12 files changed, 6 insertions(+), 525 deletions(-) diff --git a/pkg/controller/servicetopology/adapter/adapter.go b/pkg/controller/servicetopology/adapter/adapter.go index 79ec07fdbb9..3446f4cae46 100644 --- a/pkg/controller/servicetopology/adapter/adapter.go +++ b/pkg/controller/servicetopology/adapter/adapter.go @@ -25,7 +25,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/cache" "github.com/openyurtio/openyurt/pkg/yurthub/filter/servicetopology" @@ -33,7 +32,6 @@ import ( type Adapter interface { GetEnqueueKeysBySvc(svc *corev1.Service) []string - GetEnqueueKeysByNodePool(svcTopologyTypes map[string]string, allNpNodes sets.String) []string UpdateTriggerAnnotations(namespace, name string) error } diff --git a/pkg/controller/servicetopology/adapter/endpoints_adapter.go b/pkg/controller/servicetopology/adapter/endpoints_adapter.go index 4b944fd04a9..7a7c7d86d77 100644 --- a/pkg/controller/servicetopology/adapter/endpoints_adapter.go +++ b/pkg/controller/servicetopology/adapter/endpoints_adapter.go @@ -22,11 +22,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/kubernetes" - "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -47,39 +44,6 @@ func (s *endpoints) GetEnqueueKeysBySvc(svc *corev1.Service) []string { return appendKeys(keys, svc) } -func (s *endpoints) GetEnqueueKeysByNodePool(svcTopologyTypes map[string]string, allNpNodes sets.String) []string { - var keys []string - endpointsList := &corev1.EndpointsList{} - if err := s.client.List(context.TODO(), endpointsList, &client.ListOptions{LabelSelector: labels.Everything()}); err != nil { - klog.V(4).Infof("Error listing endpoints sets: %v", err) - return keys - } - - for _, ep := range endpointsList.Items { - if !isNodePoolTypeSvc(ep.Namespace, ep.Name, svcTopologyTypes) { - continue - } - - if s.getNodesInEp(&ep).Intersection(allNpNodes).Len() == 0 { - continue - } - keys = appendKeys(keys, &ep) - } - return keys -} - -func (s *endpoints) getNodesInEp(ep *corev1.Endpoints) sets.String { - nodes := sets.NewString() - for _, subset := range ep.Subsets { - for _, addr := range subset.Addresses { - if addr.NodeName != nil { - nodes.Insert(*addr.NodeName) - } - } - } - return nodes -} - func (s *endpoints) UpdateTriggerAnnotations(namespace, name string) error { patch := getUpdateTriggerPatch() _, err := s.kubeClient.CoreV1().Endpoints(namespace).Patch(context.Background(), name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) diff --git a/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go b/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go index 428d2e1fb65..1f4788d2e60 100644 --- a/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go +++ b/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go @@ -19,84 +19,16 @@ package adapter import ( "context" - "fmt" "reflect" "testing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/tools/cache" - "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func TestEndpointAdapterGetEnqueueKeysByNodePool(t *testing.T) { - svcName := "svc1-np7sf" - svcNamespace := "default" - svcKey := fmt.Sprintf("%s/%s", svcNamespace, svcName) - nodeName1 := "node1" - nodeName2 := "node2" - tcases := map[string]struct { - kubeClient kubernetes.Interface - client client.Client - nodepoolNodes sets.String - svcTopologyTypes map[string]string - expectResult []string - }{ - "service topology type: kubernetes.io/hostname": { - kubeClient: fake.NewSimpleClientset( - getEndpoints(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getEndpoints(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName1), - svcTopologyTypes: map[string]string{ - svcKey: "kubernetes.io/hostname", - }, - expectResult: nil, - }, - "service topology type: kubernetes.io/zone, don't contain nodepool nodes": { - kubeClient: fake.NewSimpleClientset( - getEndpoints(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getEndpoints(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName2), - svcTopologyTypes: map[string]string{ - svcKey: "kubernetes.io/zone", - }, - expectResult: nil, - }, - "service topology type: kubernetes.io/zone, contain nodepool nodes": { - kubeClient: fake.NewSimpleClientset( - getEndpoints(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getEndpoints(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName1), - svcTopologyTypes: map[string]string{ - svcKey: "kubernetes.io/zone", - }, - expectResult: []string{ - getCacheKey(getEndpoints(svcNamespace, svcName, nodeName1)), - }, - }, - } - - for k, tt := range tcases { - t.Logf("current test case is %s", k) - stopper := make(chan struct{}) - defer close(stopper) - - adapter := NewEndpointsAdapter(tt.kubeClient, tt.client) - keys := adapter.GetEnqueueKeysByNodePool(tt.svcTopologyTypes, tt.nodepoolNodes) - if !reflect.DeepEqual(keys, tt.expectResult) { - t.Errorf("expect enqueue keys %v, but got %v", tt.expectResult, keys) - } - - } -} - func TestEndpointAdapterUpdateTriggerAnnotations(t *testing.T) { ep := getEndpoints("default", "svc1", "node1") diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go b/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go index b9b024bb6eb..72814822e85 100644 --- a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go +++ b/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go @@ -23,9 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" @@ -58,39 +56,6 @@ func (s *endpointslicev1) GetEnqueueKeysBySvc(svc *corev1.Service) []string { return keys } -func (s *endpointslicev1) GetEnqueueKeysByNodePool(svcTopologyTypes map[string]string, allNpNodes sets.String) []string { - var keys []string - epSliceList := &discoveryv1.EndpointSliceList{} - if err := s.client.List(context.TODO(), epSliceList, &client.ListOptions{LabelSelector: labels.Everything()}); err != nil { - klog.V(4).Infof("Error listing endpointslices sets: %v", err) - return keys - } - - for _, epSlice := range epSliceList.Items { - svcNamespace := epSlice.Namespace - svcName := epSlice.Labels[discoveryv1.LabelServiceName] - if !isNodePoolTypeSvc(svcNamespace, svcName, svcTopologyTypes) { - continue - } - if s.getNodesInEpSlice(&epSlice).Intersection(allNpNodes).Len() == 0 { - continue - } - keys = appendKeys(keys, &epSlice) - } - - return keys -} - -func (s *endpointslicev1) getNodesInEpSlice(epSlice *discoveryv1.EndpointSlice) sets.String { - nodes := sets.NewString() - for _, ep := range epSlice.Endpoints { - if ep.NodeName != nil { - nodes.Insert(*ep.NodeName) - } - } - return nodes -} - func (s *endpointslicev1) UpdateTriggerAnnotations(namespace, name string) error { patch := getUpdateTriggerPatch() _, err := s.kubeClient.DiscoveryV1().EndpointSlices(namespace).Patch(context.Background(), name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go b/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go index 935d4f824f0..d8719994333 100644 --- a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go +++ b/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go @@ -26,78 +26,10 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" - "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func TestEndpointSliceAdapterGetEnqueueKeysByNodePool(t *testing.T) { - svcName := "svc1" - svcNamespace := "default" - svcKey := fmt.Sprintf("%s/%s", svcNamespace, svcName) - nodeName1 := "node1" - nodeName2 := "node2" - tcases := map[string]struct { - kubeClient kubernetes.Interface - client client.Client - nodepoolNodes sets.String - svcTopologyTypes map[string]string - expectResult []string - }{ - "service topology type: kubernetes.io/hostname": { - kubeClient: fake.NewSimpleClientset( - getEndpointSlice(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getEndpointSlice(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName1), - svcTopologyTypes: map[string]string{ - svcKey: "kubernetes.io/hostname", - }, - expectResult: nil, - }, - "service topology type: kubernetes.io/zone, don't contain nodepool nodes": { - kubeClient: fake.NewSimpleClientset( - getEndpointSlice(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getEndpointSlice(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName2), - svcTopologyTypes: map[string]string{ - svcKey: "kubernetes.io/zone", - }, - expectResult: nil, - }, - "service topology type: kubernetes.io/zone, contain nodepool nodes": { - kubeClient: fake.NewSimpleClientset( - getEndpointSlice(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getEndpointSlice(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName1), - svcTopologyTypes: map[string]string{ - svcKey: "kubernetes.io/zone", - }, - expectResult: []string{ - getCacheKey(getEndpointSlice(svcNamespace, svcName, nodeName1)), - }, - }, - } - - for k, tt := range tcases { - t.Logf("current test case is %s", k) - - stopper := make(chan struct{}) - defer close(stopper) - - adapter := NewEndpointsV1Adapter(tt.kubeClient, tt.client) - keys := adapter.GetEnqueueKeysByNodePool(tt.svcTopologyTypes, tt.nodepoolNodes) - if !reflect.DeepEqual(keys, tt.expectResult) { - t.Errorf("expect enqueue keys %v, but got %v", tt.expectResult, keys) - } - - } -} - func TestEndpointSliceV1AdapterUpdateTriggerAnnotations(t *testing.T) { svcName := "svc1" svcNamespace := "default" diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go b/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go index 9b81c5bf690..5c35ea7f95f 100644 --- a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go +++ b/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go @@ -23,9 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" @@ -58,40 +56,6 @@ func (s *endpointslicev1beta1) GetEnqueueKeysBySvc(svc *corev1.Service) []string return keys } -func (s *endpointslicev1beta1) GetEnqueueKeysByNodePool(svcTopologyTypes map[string]string, allNpNodes sets.String) []string { - var keys []string - epSliceList := &discoveryv1beta1.EndpointSliceList{} - if err := s.client.List(context.TODO(), epSliceList, &client.ListOptions{LabelSelector: labels.Everything()}); err != nil { - klog.V(4).Infof("Error listing endpointslices sets: %v", err) - return keys - } - - for _, epSlice := range epSliceList.Items { - svcNamespace := epSlice.Namespace - svcName := epSlice.Labels[discoveryv1beta1.LabelServiceName] - if !isNodePoolTypeSvc(svcNamespace, svcName, svcTopologyTypes) { - continue - } - if s.getNodesInEpSlice(&epSlice).Intersection(allNpNodes).Len() == 0 { - continue - } - keys = appendKeys(keys, &epSlice) - } - - return keys -} - -func (s *endpointslicev1beta1) getNodesInEpSlice(epSlice *discoveryv1beta1.EndpointSlice) sets.String { - nodes := sets.NewString() - for _, ep := range epSlice.Endpoints { - nodeName, ok := ep.Topology[corev1.LabelHostname] - if ok { - nodes.Insert(nodeName) - } - } - return nodes -} - func (s *endpointslicev1beta1) UpdateTriggerAnnotations(namespace, name string) error { patch := getUpdateTriggerPatch() _, err := s.kubeClient.DiscoveryV1beta1().EndpointSlices(namespace).Patch(context.Background(), name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go b/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go index 0081a5134d4..a9a874179b4 100644 --- a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go +++ b/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go @@ -22,87 +22,14 @@ import ( "fmt" "reflect" "testing" - "time" corev1 "k8s.io/api/core/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" - "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -func TestEndpointSliceV1Beta1AdapterGetEnqueueKeysByNodePool(t *testing.T) { - svcName := "svc1" - svcNamespace := "default" - svcKey := fmt.Sprintf("%s/%s", svcNamespace, svcName) - nodeName1 := "node1" - nodeName2 := "node2" - tcases := map[string]struct { - kubeClient kubernetes.Interface - client client.Client - nodepoolNodes sets.String - svcTopologyTypes map[string]string - expectResult []string - }{ - "service topology type: kubernetes.io/hostname": { - kubeClient: fake.NewSimpleClientset( - getV1Beta1EndpointSlice(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getV1Beta1EndpointSlice(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName1), - svcTopologyTypes: map[string]string{ - svcKey: "kubernetes.io/hostname", - }, - expectResult: nil, - }, - "service topology type: openyurt.io/nodepool, don't contain nodepool nodes": { - kubeClient: fake.NewSimpleClientset( - getV1Beta1EndpointSlice(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getV1Beta1EndpointSlice(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName2), - svcTopologyTypes: map[string]string{ - svcKey: "openyurt.io/nodepool", - }, - expectResult: nil, - }, - "service topology type: kubernetes.io/zone, contain nodepool nodes": { - kubeClient: fake.NewSimpleClientset( - getV1Beta1EndpointSlice(svcNamespace, svcName, nodeName1), - ), - client: fakeclient.NewClientBuilder().WithObjects(getV1Beta1EndpointSlice(svcNamespace, svcName, nodeName1)).Build(), - nodepoolNodes: sets.NewString(nodeName1), - svcTopologyTypes: map[string]string{ - svcKey: "kubernetes.io/zone", - }, - expectResult: []string{ - getCacheKey(getV1Beta1EndpointSlice(svcNamespace, svcName, nodeName1)), - }, - }, - } - - for k, tt := range tcases { - t.Logf("current test case is %s", k) - factory := informers.NewSharedInformerFactory(tt.kubeClient, 24*time.Hour) - - stopper := make(chan struct{}) - defer close(stopper) - factory.Start(stopper) - factory.WaitForCacheSync(stopper) - - adapter := NewEndpointsV1Beta1Adapter(tt.kubeClient, tt.client) - keys := adapter.GetEnqueueKeysByNodePool(tt.svcTopologyTypes, tt.nodepoolNodes) - if !reflect.DeepEqual(keys, tt.expectResult) { - t.Errorf("expect enqueue keys %v, but got %v", tt.expectResult, keys) - } - - } -} - func TestEndpointSliceV1Beta1AdapterUpdateTriggerAnnotations(t *testing.T) { svcName := "svc1" svcNamespace := "default" diff --git a/pkg/controller/servicetopology/endpoints/endpoints_controller.go b/pkg/controller/servicetopology/endpoints/endpoints_controller.go index 463093b643f..228c6cc0c9a 100644 --- a/pkg/controller/servicetopology/endpoints/endpoints_controller.go +++ b/pkg/controller/servicetopology/endpoints/endpoints_controller.go @@ -22,10 +22,8 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "k8s.io/client-go/tools/record" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,10 +33,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" common "github.com/openyurtio/openyurt/pkg/controller/servicetopology" "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" - utilclient "github.com/openyurtio/openyurt/pkg/util/client" utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" ) @@ -72,18 +68,17 @@ var _ reconcile.Reconciler = &ReconcileServicetopologyEndpoints{} // ReconcileServicetopologyEndpoints reconciles a endpoints object type ReconcileServicetopologyEndpoints struct { client.Client - scheme *runtime.Scheme - recorder record.EventRecorder endpointsAdapter adapter.Adapter } // newReconciler returns a new reconcile.Reconciler func newReconciler(_ *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { - return &ReconcileServicetopologyEndpoints{ - Client: utilclient.NewClientFromManager(mgr, common.ControllerName), - scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(common.ControllerName), - } + return &ReconcileServicetopologyEndpoints{} +} + +func (r *ReconcileServicetopologyEndpoints) InjectClient(c client.Client) error { + r.Client = c + return nil } func (r *ReconcileServicetopologyEndpoints) InjectConfig(cfg *rest.Config) error { @@ -111,14 +106,6 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } - // Watch for changes to NodePool - if err := c.Watch(&source.Kind{Type: &appsv1beta1.NodePool{}}, &EnqueueEndpointsForNodePool{ - endpointsAdapter: r.(*ReconcileServicetopologyEndpoints).endpointsAdapter, - client: r.(*ReconcileServicetopologyEndpoints).Client, - }); err != nil { - return err - } - return nil } diff --git a/pkg/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go b/pkg/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go index 18aa9aa41cc..2ac0c355fb6 100644 --- a/pkg/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go +++ b/pkg/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go @@ -19,17 +19,14 @@ package endpoints import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" "github.com/openyurtio/openyurt/pkg/controller/servicetopology/util" - nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" ) type EnqueueEndpointsForService struct { @@ -85,74 +82,3 @@ func (e *EnqueueEndpointsForService) enqueueEndpointsForSvc(newSvc *corev1.Servi }) } } - -type EnqueueEndpointsForNodePool struct { - endpointsAdapter adapter.Adapter - client client.Client -} - -// Create implements EventHandler -func (e *EnqueueEndpointsForNodePool) Create(evt event.CreateEvent, - q workqueue.RateLimitingInterface) { -} - -// Update implements EventHandler -func (e *EnqueueEndpointsForNodePool) Update(evt event.UpdateEvent, - q workqueue.RateLimitingInterface) { - oldNp, ok := evt.ObjectOld.(*nodepoolv1alpha1.NodePool) - if !ok { - klog.Errorf(Format("Fail to assert runtime Object(%s) to nodepoolv1alpha1.NodePool", - evt.ObjectOld.GetName())) - return - } - newNp, ok := evt.ObjectNew.(*nodepoolv1alpha1.NodePool) - if !ok { - klog.Errorf(Format("Fail to assert runtime Object(%s) to nodepoolv1alpha1.NodePool", - evt.ObjectNew.GetName())) - return - } - newNpNodes := sets.NewString(newNp.Status.Nodes...) - oldNpNodes := sets.NewString(oldNp.Status.Nodes...) - if newNpNodes.Equal(oldNpNodes) { - return - } - klog.Infof(Format("the nodes record of nodepool %s is changed from %v to %v.", newNp.Name, oldNp.Status.Nodes, newNp.Status.Nodes)) - allNpNodes := newNpNodes.Union(oldNpNodes) - svcTopologyTypes := util.GetSvcTopologyTypes(e.client) - e.enqueueEndpointsForNodePool(svcTopologyTypes, allNpNodes, newNp, q) -} - -// Delete implements EventHandler -func (e *EnqueueEndpointsForNodePool) Delete(evt event.DeleteEvent, - q workqueue.RateLimitingInterface) { - nodePool, ok := evt.Object.(*nodepoolv1alpha1.NodePool) - if !ok { - klog.Errorf(Format("Fail to assert runtime Object(%s) to nodepoolv1alpha1.NodePool", - evt.Object.GetName())) - return - } - klog.Infof(Format("nodepool %s is deleted", nodePool.Name)) - allNpNodes := sets.NewString(nodePool.Status.Nodes...) - svcTopologyTypes := util.GetSvcTopologyTypes(e.client) - e.enqueueEndpointsForNodePool(svcTopologyTypes, allNpNodes, nodePool, q) -} - -// Generic implements EventHandler -func (e *EnqueueEndpointsForNodePool) Generic(evt event.GenericEvent, - q workqueue.RateLimitingInterface) { -} - -func (e *EnqueueEndpointsForNodePool) enqueueEndpointsForNodePool(svcTopologyTypes map[string]string, allNpNodes sets.String, np *nodepoolv1alpha1.NodePool, q workqueue.RateLimitingInterface) { - keys := e.endpointsAdapter.GetEnqueueKeysByNodePool(svcTopologyTypes, allNpNodes) - klog.Infof(Format("according to the change of the nodepool %s, enqueue endpoints: %v", np.Name, keys)) - for _, key := range keys { - ns, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - klog.Errorf("failed to split key %s, %v", key, err) - continue - } - q.AddRateLimited(reconcile.Request{ - NamespacedName: types.NamespacedName{Namespace: ns, Name: name}, - }) - } -} diff --git a/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go b/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go index 49cd3cadeed..41c91cc912b 100644 --- a/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go +++ b/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go @@ -36,7 +36,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" common "github.com/openyurtio/openyurt/pkg/controller/servicetopology" "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" ) @@ -77,14 +76,6 @@ func Add(_ *appconfig.CompletedConfig, mgr manager.Manager) error { return err } - // Watch for changes to NodePool - if err := c.Watch(&source.Kind{Type: &appsv1beta1.NodePool{}}, &EnqueueEndpointsliceForNodePool{ - endpointsliceAdapter: r.endpointsliceAdapter, - client: r.Client, - }); err != nil { - return err - } - klog.Infof("%s-endpointslice controller is added", common.ControllerName) return nil } diff --git a/pkg/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go b/pkg/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go index 9fb2712a4a6..1d39a4c5f56 100644 --- a/pkg/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go +++ b/pkg/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go @@ -19,17 +19,14 @@ package endpointslice import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" "github.com/openyurtio/openyurt/pkg/controller/servicetopology/util" - nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" ) type EnqueueEndpointsliceForService struct { @@ -85,74 +82,3 @@ func (e *EnqueueEndpointsliceForService) enqueueEndpointsliceForSvc(newSvc *core }) } } - -type EnqueueEndpointsliceForNodePool struct { - endpointsliceAdapter adapter.Adapter - client client.Client -} - -// Create implements EventHandler -func (e *EnqueueEndpointsliceForNodePool) Create(evt event.CreateEvent, - q workqueue.RateLimitingInterface) { -} - -// Update implements EventHandler -func (e *EnqueueEndpointsliceForNodePool) Update(evt event.UpdateEvent, - q workqueue.RateLimitingInterface) { - oldNp, ok := evt.ObjectOld.(*nodepoolv1alpha1.NodePool) - if !ok { - klog.Errorf(Format("Fail to assert runtime Object(%s) to nodepoolv1alpha1.NodePool", - evt.ObjectOld.GetName())) - return - } - newNp, ok := evt.ObjectNew.(*nodepoolv1alpha1.NodePool) - if !ok { - klog.Errorf(Format("Fail to assert runtime Object(%s) to nodepoolv1alpha1.NodePool", - evt.ObjectNew.GetName())) - return - } - newNpNodes := sets.NewString(newNp.Status.Nodes...) - oldNpNodes := sets.NewString(oldNp.Status.Nodes...) - if newNpNodes.Equal(oldNpNodes) { - return - } - klog.Infof(Format("the nodes record of nodepool %s is changed from %v to %v.", newNp.Name, oldNp.Status.Nodes, newNp.Status.Nodes)) - allNpNodes := newNpNodes.Union(oldNpNodes) - svcTopologyTypes := util.GetSvcTopologyTypes(e.client) - e.enqueueEndpointSliceForNodePool(svcTopologyTypes, allNpNodes, newNp, q) -} - -// Delete implements EventHandler -func (e *EnqueueEndpointsliceForNodePool) Delete(evt event.DeleteEvent, - q workqueue.RateLimitingInterface) { - nodePool, ok := evt.Object.(*nodepoolv1alpha1.NodePool) - if !ok { - klog.Errorf(Format("Fail to assert runtime Object(%s) to nodepoolv1alpha1.NodePool", - evt.Object.GetName())) - return - } - klog.Infof(Format("nodepool %s is deleted", nodePool.Name)) - allNpNodes := sets.NewString(nodePool.Status.Nodes...) - svcTopologyTypes := util.GetSvcTopologyTypes(e.client) - e.enqueueEndpointSliceForNodePool(svcTopologyTypes, allNpNodes, nodePool, q) -} - -// Generic implements EventHandler -func (e *EnqueueEndpointsliceForNodePool) Generic(evt event.GenericEvent, - q workqueue.RateLimitingInterface) { -} - -func (e *EnqueueEndpointsliceForNodePool) enqueueEndpointSliceForNodePool(svcTopologyTypes map[string]string, allNpNodes sets.String, np *nodepoolv1alpha1.NodePool, q workqueue.RateLimitingInterface) { - keys := e.endpointsliceAdapter.GetEnqueueKeysByNodePool(svcTopologyTypes, allNpNodes) - klog.Infof(Format("according to the change of the nodepool %s, enqueue endpointslice: %v", np.Name, keys)) - for _, key := range keys { - ns, name, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - klog.Errorf("failed to split key %s, %v", key, err) - continue - } - q.AddRateLimited(reconcile.Request{ - NamespacedName: types.NamespacedName{Namespace: ns, Name: name}, - }) - } -} diff --git a/pkg/controller/servicetopology/util/util.go b/pkg/controller/servicetopology/util/util.go index 37fabb2debb..5ab9e16a1e8 100644 --- a/pkg/controller/servicetopology/util/util.go +++ b/pkg/controller/servicetopology/util/util.go @@ -17,14 +17,7 @@ limitations under the License. package util import ( - "context" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/tools/cache" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" "github.com/openyurtio/openyurt/pkg/yurthub/filter/servicetopology" ) @@ -37,27 +30,3 @@ func ServiceTopologyTypeChanged(oldSvc, newSvc *corev1.Service) bool { } return true } - -func GetSvcTopologyTypes(c client.Client) map[string]string { - svcTopologyTypes := make(map[string]string) - svcList := &corev1.ServiceList{} - if err := c.List(context.TODO(), svcList, &client.ListOptions{LabelSelector: labels.Everything()}); err != nil { - klog.V(4).Infof("failed to list service sets: %v", err) - return svcTopologyTypes - } - - for _, svc := range svcList.Items { - topologyType, ok := svc.Annotations[servicetopology.AnnotationServiceTopologyKey] - if !ok { - continue - } - - key, err := cache.MetaNamespaceKeyFunc(svc) - if err != nil { - runtime.HandleError(err) - continue - } - svcTopologyTypes[key] = topologyType - } - return svcTopologyTypes -} From 1d96f3ba03405ee7580ffeb49ab510763a16f698 Mon Sep 17 00:00:00 2001 From: rambohe Date: Sun, 18 Jun 2023 23:20:12 +0800 Subject: [PATCH 33/93] delete unused functions (#1555) --- .../servicetopology/adapter/adapter.go | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/pkg/controller/servicetopology/adapter/adapter.go b/pkg/controller/servicetopology/adapter/adapter.go index 3446f4cae46..dd559dd70c2 100644 --- a/pkg/controller/servicetopology/adapter/adapter.go +++ b/pkg/controller/servicetopology/adapter/adapter.go @@ -22,12 +22,9 @@ import ( "time" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/tools/cache" - - "github.com/openyurtio/openyurt/pkg/yurthub/filter/servicetopology" ) type Adapter interface { @@ -53,36 +50,6 @@ func appendKeys(keys []string, obj interface{}) []string { return keys } -func isNodePoolTypeSvc(namespace, name string, svcTopologyTypes map[string]string) bool { - svc := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - } - key, err := cache.MetaNamespaceKeyFunc(svc) - if err != nil { - runtime.HandleError(err) - return false - } - - return isNodePoolTypeTopology(svcTopologyTypes[key]) -} - -// TODO: if service topology need to support multi types, -// like openyurt.io/topologyKeys: "kubernetes.io/hostname, openyurt.io/nodepool, *". -// For simplicity,as long as the value of service topology annotation contains nodepool type, -// then this topology is recognized as nodepool type -func isNodePoolTypeTopology(topologyType string) bool { - if topologyType == servicetopology.AnnotationServiceTopologyValueNodePool { - return true - } - if topologyType == servicetopology.AnnotationServiceTopologyValueZone { - return true - } - return false -} - func getUpdateTriggerPatch() []byte { patch := fmt.Sprintf(`{"metadata":{"annotations": {"openyurt.io/update-trigger": "%d"}}}`, time.Now().Unix()) return []byte(patch) From 669e004e8f52a3f3e94c895e75d307f56886bff9 Mon Sep 17 00:00:00 2001 From: rambohe Date: Mon, 19 Jun 2023 10:08:12 +0800 Subject: [PATCH 34/93] fix conflicts for getting node by local storage in yurthub filters (#1552) --- cmd/yurthub/app/config/config.go | 2 +- pkg/yurthub/filter/filter.go | 11 +- pkg/yurthub/filter/initializer/initializer.go | 36 ++-- pkg/yurthub/filter/manager/manager.go | 11 +- pkg/yurthub/filter/manager/manager_test.go | 54 +++--- .../filter/nodeportisolation/filter.go | 126 +++----------- .../filter/nodeportisolation/filter_test.go | 86 ++++------ pkg/yurthub/filter/servicetopology/filter.go | 118 +++---------- .../filter/servicetopology/filter_test.go | 161 +++++++++--------- 9 files changed, 206 insertions(+), 399 deletions(-) diff --git a/cmd/yurthub/app/config/config.go b/cmd/yurthub/app/config/config.go index b209a16118e..88f35cfa93a 100644 --- a/cmd/yurthub/app/config/config.go +++ b/cmd/yurthub/app/config/config.go @@ -139,7 +139,7 @@ func Complete(options *options.YurtHubOptions) (*YurtHubConfiguration, error) { } tenantNs := util.ParseTenantNsFromOrgs(options.YurtHubCertOrganizations) registerInformers(options, sharedFactory, yurtSharedFactory, workingMode, tenantNs) - filterManager, err := manager.NewFilterManager(options, sharedFactory, yurtSharedFactory, serializerManager, storageWrapper, us[0].Host) + filterManager, err := manager.NewFilterManager(options, sharedFactory, yurtSharedFactory, proxiedClient, serializerManager, us[0].Host) if err != nil { klog.Errorf("could not create filter manager, %v", err) return nil, err diff --git a/pkg/yurthub/filter/filter.go b/pkg/yurthub/filter/filter.go index f9e0d1b09c6..9700fb4a3d6 100644 --- a/pkg/yurthub/filter/filter.go +++ b/pkg/yurthub/filter/filter.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "strings" "sync" "k8s.io/apimachinery/pkg/runtime" @@ -274,15 +275,11 @@ func CreateFilterChain(objFilters []ObjectFilter) ObjectFilter { } func (chain filterChain) Name() string { - var name string + var names []string for i := range chain { - if len(name) == 0 { - name = chain[i].Name() - } else { - name = "," + chain[i].Name() - } + names = append(names, chain[i].Name()) } - return name + return strings.Join(names, ",") } func (chain filterChain) SupportedResourceAndVerbs() map[string]sets.String { diff --git a/pkg/yurthub/filter/initializer/initializer.go b/pkg/yurthub/filter/initializer/initializer.go index fe120463a28..796c4a1b0d1 100644 --- a/pkg/yurthub/filter/initializer/initializer.go +++ b/pkg/yurthub/filter/initializer/initializer.go @@ -18,10 +18,9 @@ package initializer import ( "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" - "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/filter" - "github.com/openyurtio/openyurt/pkg/yurthub/util" yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" ) @@ -45,59 +44,46 @@ type WantsNodePoolName interface { SetNodePoolName(nodePoolName string) error } -// WantsStorageWrapper is an interface for setting StorageWrapper -type WantsStorageWrapper interface { - SetStorageWrapper(s cachemanager.StorageWrapper) error -} - // WantsMasterServiceAddr is an interface for setting mutated master service address type WantsMasterServiceAddr interface { SetMasterServiceHost(host string) error SetMasterServicePort(port string) error } -// WantsWorkingMode is an interface for setting working mode -type WantsWorkingMode interface { - SetWorkingMode(mode util.WorkingMode) error +// WantsKubeClient is an interface for setting kube client +type WantsKubeClient interface { + SetKubeClient(client kubernetes.Interface) error } // genericFilterInitializer is responsible for initializing generic filter type genericFilterInitializer struct { factory informers.SharedInformerFactory yurtFactory yurtinformers.SharedInformerFactory - storageWrapper cachemanager.StorageWrapper nodeName string nodePoolName string masterServiceHost string masterServicePort string - workingMode util.WorkingMode + client kubernetes.Interface } // New creates an filterInitializer object func New(factory informers.SharedInformerFactory, yurtFactory yurtinformers.SharedInformerFactory, - sw cachemanager.StorageWrapper, - nodeName, nodePoolName, masterServiceHost, masterServicePort string, - workingMode util.WorkingMode) *genericFilterInitializer { + kubeClient kubernetes.Interface, + nodeName, nodePoolName, masterServiceHost, masterServicePort string) *genericFilterInitializer { return &genericFilterInitializer{ factory: factory, yurtFactory: yurtFactory, - storageWrapper: sw, nodeName: nodeName, + nodePoolName: nodePoolName, masterServiceHost: masterServiceHost, masterServicePort: masterServicePort, - workingMode: workingMode, + client: kubeClient, } } // Initialize used for executing filter initialization func (fi *genericFilterInitializer) Initialize(ins filter.ObjectFilter) error { - if wants, ok := ins.(WantsWorkingMode); ok { - if err := wants.SetWorkingMode(fi.workingMode); err != nil { - return err - } - } - if wants, ok := ins.(WantsNodeName); ok { if err := wants.SetNodeName(fi.nodeName); err != nil { return err @@ -132,8 +118,8 @@ func (fi *genericFilterInitializer) Initialize(ins filter.ObjectFilter) error { } } - if wants, ok := ins.(WantsStorageWrapper); ok { - if err := wants.SetStorageWrapper(fi.storageWrapper); err != nil { + if wants, ok := ins.(WantsKubeClient); ok { + if err := wants.SetKubeClient(fi.client); err != nil { return err } } diff --git a/pkg/yurthub/filter/manager/manager.go b/pkg/yurthub/filter/manager/manager.go index a60f3bb3e1b..bc3d5f5dd91 100644 --- a/pkg/yurthub/filter/manager/manager.go +++ b/pkg/yurthub/filter/manager/manager.go @@ -23,9 +23,9 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" "github.com/openyurtio/openyurt/cmd/yurthub/app/options" - "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/filter" "github.com/openyurtio/openyurt/pkg/yurthub/filter/discardcloudservice" "github.com/openyurtio/openyurt/pkg/yurthub/filter/inclusterconfig" @@ -47,8 +47,8 @@ type Manager struct { func NewFilterManager(options *options.YurtHubOptions, sharedFactory informers.SharedInformerFactory, yurtSharedFactory yurtinformers.SharedInformerFactory, + proxiedClient kubernetes.Interface, serializerManager *serializer.SerializerManager, - storageWrapper cachemanager.StorageWrapper, apiserverAddr string) (*Manager, error) { if !options.EnableResourceFilter { return nil, nil @@ -70,7 +70,7 @@ func NewFilterManager(options *options.YurtHubOptions, } } - objFilters, err := createObjectFilters(filters, sharedFactory, yurtSharedFactory, storageWrapper, util.WorkingMode(options.WorkingMode), options.NodeName, options.NodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort) + objFilters, err := createObjectFilters(filters, sharedFactory, yurtSharedFactory, proxiedClient, options.NodeName, options.NodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort) if err != nil { return nil, err } @@ -114,14 +114,13 @@ func (m *Manager) FindResponseFilter(req *http.Request) (filter.ResponseFilter, func createObjectFilters(filters *filter.Filters, sharedFactory informers.SharedInformerFactory, yurtSharedFactory yurtinformers.SharedInformerFactory, - storageWrapper cachemanager.StorageWrapper, - workingMode util.WorkingMode, + proxiedClient kubernetes.Interface, nodeName, nodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort string) ([]filter.ObjectFilter, error) { if filters == nil { return nil, nil } - genericInitializer := initializer.New(sharedFactory, yurtSharedFactory, storageWrapper, nodeName, nodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort, workingMode) + genericInitializer := initializer.New(sharedFactory, yurtSharedFactory, proxiedClient, nodeName, nodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort) initializerChain := filter.Initializers{} initializerChain = append(initializerChain, genericInitializer) return filters.NewFromFilters(initializerChain) diff --git a/pkg/yurthub/filter/manager/manager_test.go b/pkg/yurthub/filter/manager/manager_test.go index 56e17923d6b..e702b7cd1b9 100644 --- a/pkg/yurthub/filter/manager/manager_test.go +++ b/pkg/yurthub/filter/manager/manager_test.go @@ -30,11 +30,9 @@ import ( "k8s.io/client-go/kubernetes/fake" "github.com/openyurtio/openyurt/cmd/yurthub/app/options" - "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/filter" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer" "github.com/openyurtio/openyurt/pkg/yurthub/proxy/util" - "github.com/openyurtio/openyurt/pkg/yurthub/storage/disk" yurtfake "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned/fake" yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" ) @@ -42,19 +40,8 @@ import ( func TestFindResponseFilter(t *testing.T) { fakeClient := &fake.Clientset{} fakeYurtClient := &yurtfake.Clientset{} - sharedFactory, yurtSharedFactory := informers.NewSharedInformerFactory(fakeClient, 24*time.Hour), - yurtinformers.NewSharedInformerFactory(fakeYurtClient, 24*time.Hour) serializerManager := serializer.NewSerializerManager() - storageManager, err := disk.NewDiskStorage("/tmp/filter_manager") - if err != nil { - t.Fatalf("could not create storage manager, %v", err) - } - storageWrapper := cachemanager.NewStorageWrapper(storageManager) apiserverAddr := "127.0.0.1:6443" - stopper := make(chan struct{}) - defer close(stopper) - sharedFactory.Start(stopper) - yurtSharedFactory.Start(stopper) testcases := map[string]struct { enableResourceFilter bool @@ -67,7 +54,7 @@ func TestFindResponseFilter(t *testing.T) { path string mgrIsNil bool isFound bool - names []string + names sets.String }{ "disable resource filter": { enableResourceFilter: false, @@ -81,9 +68,9 @@ func TestFindResponseFilter(t *testing.T) { verb: "GET", path: "/api/v1/services", isFound: true, - names: []string{"masterservice"}, + names: sets.NewString("masterservice"), }, - "get discard cloud service filter": { + "get discard cloud service and node port isolation filter": { enableResourceFilter: true, accessServerThroughHub: true, enableDummyIf: true, @@ -91,7 +78,7 @@ func TestFindResponseFilter(t *testing.T) { verb: "GET", path: "/api/v1/services", isFound: true, - names: []string{"discardcloudservice"}, + names: sets.NewString("discardcloudservice", "nodeportisolation"), }, "get service topology filter": { enableResourceFilter: true, @@ -101,7 +88,7 @@ func TestFindResponseFilter(t *testing.T) { verb: "GET", path: "/api/v1/endpoints", isFound: true, - names: []string{"servicetopology"}, + names: sets.NewString("servicetopology"), }, "disable service topology filter": { enableResourceFilter: true, @@ -120,7 +107,8 @@ func TestFindResponseFilter(t *testing.T) { userAgent: "kube-proxy", verb: "GET", path: "/api/v1/services", - isFound: false, + isFound: true, + names: sets.NewString("nodeportisolation"), }, } @@ -140,14 +128,19 @@ func TestFindResponseFilter(t *testing.T) { } options.DisabledResourceFilters = append(options.DisabledResourceFilters, tt.disabledResourceFilters...) - mgr, _ := NewFilterManager(options, sharedFactory, yurtSharedFactory, serializerManager, storageWrapper, apiserverAddr) - if tt.mgrIsNil && mgr != nil { - t.Errorf("expect manager is nil, but got not nil: %v", mgr) - } else { - // mgr is nil, complete this test case + sharedFactory, yurtSharedFactory := informers.NewSharedInformerFactory(fakeClient, 24*time.Hour), + yurtinformers.NewSharedInformerFactory(fakeYurtClient, 24*time.Hour) + stopper := make(chan struct{}) + defer close(stopper) + + mgr, _ := NewFilterManager(options, sharedFactory, yurtSharedFactory, fakeClient, serializerManager, apiserverAddr) + if tt.mgrIsNil && mgr == nil { return } + sharedFactory.Start(stopper) + yurtSharedFactory.Start(stopper) + req, err := http.NewRequest(tt.verb, tt.path, nil) if err != nil { t.Errorf("failed to create request, %v", err) @@ -168,20 +161,13 @@ func TestFindResponseFilter(t *testing.T) { handler = filters.WithRequestInfo(handler, resolver) handler.ServeHTTP(httptest.NewRecorder(), req) - if isFound != tt.isFound { - t.Errorf("expect isFound %v, but got %v", tt.isFound, isFound) + if !tt.isFound && isFound == tt.isFound { return } names := strings.Split(responseFilter.Name(), ",") - if len(tt.names) != len(names) { - t.Errorf("expect filter names %v, but got %v", tt.names, names) - } - - for i := range tt.names { - if tt.names[i] != names[i] { - t.Errorf("expect filter names %v, but got %v", tt.names, names) - } + if !tt.names.Equal(sets.NewString(names...)) { + t.Errorf("expect filter names %v, but got %v", tt.names.List(), names) } }) } diff --git a/pkg/yurthub/filter/nodeportisolation/filter.go b/pkg/yurthub/filter/nodeportisolation/filter.go index f7088ce3435..c4c9ff95407 100644 --- a/pkg/yurthub/filter/nodeportisolation/filter.go +++ b/pkg/yurthub/filter/nodeportisolation/filter.go @@ -17,22 +17,19 @@ limitations under the License. package nodeportisolation import ( + "context" "fmt" "strings" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/informers" - "k8s.io/client-go/tools/cache" + "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" + "github.com/openyurtio/openyurt/pkg/apis/apps" "github.com/openyurtio/openyurt/pkg/yurthub/filter" - "github.com/openyurtio/openyurt/pkg/yurthub/storage" - "github.com/openyurtio/openyurt/pkg/yurthub/storage/disk" - "github.com/openyurtio/openyurt/pkg/yurthub/util" - nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" ) const ( @@ -48,10 +45,8 @@ func Register(filters *filter.Filters) { type nodePortIsolationFilter struct { nodePoolName string - workingMode util.WorkingMode - nodeGetter filter.NodeGetter - nodeSynced cache.InformerSynced nodeName string + client kubernetes.Interface } func (nif *nodePortIsolationFilter) Name() string { @@ -74,92 +69,12 @@ func (nif *nodePortIsolationFilter) SetNodeName(nodeName string) error { return nil } -func (nif *nodePortIsolationFilter) SetWorkingMode(mode util.WorkingMode) error { - nif.workingMode = mode - return nil -} - -func (nif *nodePortIsolationFilter) SetSharedInformerFactory(factory informers.SharedInformerFactory) error { - if nif.workingMode == util.WorkingModeCloud { - klog.Infof("prepare list/watch to sync node(%s) for cloud working mode", nif.nodeName) - nif.nodeSynced = factory.Core().V1().Nodes().Informer().HasSynced - nif.nodeGetter = factory.Core().V1().Nodes().Lister().Get - } - - return nil -} - -func (nif *nodePortIsolationFilter) SetStorageWrapper(s cachemanager.StorageWrapper) error { - if s.Name() != disk.StorageName { - return fmt.Errorf("nodePortIsolationFilter can only support disk storage currently, cannot use %s", s.Name()) - } - - if len(nif.nodeName) == 0 { - return fmt.Errorf("node name for nodePortIsolationFilter is not set") - } - - // hub agent will list/watch node from kube-apiserver when hub agent work as cloud mode - if nif.workingMode == util.WorkingModeCloud { - return nil - } - klog.Infof("prepare local disk storage to sync node(%s) for edge working mode", nif.nodeName) - - nodeKey, err := s.KeyFunc(storage.KeyBuildInfo{ - Component: "kubelet", - Name: nif.nodeName, - Resources: "nodes", - Group: "", - Version: "v1", - }) - if err != nil { - return fmt.Errorf("failed to get node key for %s, %v", nif.nodeName, err) - } - nif.nodeSynced = func() bool { - obj, err := s.Get(nodeKey) - if err != nil || obj == nil { - return false - } - - if _, ok := obj.(*v1.Node); !ok { - return false - } - - return true - } - - nif.nodeGetter = func(name string) (*v1.Node, error) { - key, err := s.KeyFunc(storage.KeyBuildInfo{ - Component: "kubelet", - Name: name, - Resources: "nodes", - Group: "", - Version: "v1", - }) - if err != nil { - return nil, fmt.Errorf("nodeGetter failed to get node key for %s, %v", name, err) - } - obj, err := s.Get(key) - if err != nil { - return nil, err - } else if obj == nil { - return nil, fmt.Errorf("node(%s) is not ready", name) - } - - if node, ok := obj.(*v1.Node); ok { - return node, nil - } - - return nil, fmt.Errorf("node(%s) is not found", name) - } - +func (nif *nodePortIsolationFilter) SetKubeClient(client kubernetes.Interface) error { + nif.client = client return nil } func (nif *nodePortIsolationFilter) Filter(obj runtime.Object, stopCh <-chan struct{}) runtime.Object { - if ok := cache.WaitForCacheSync(stopCh, nif.nodeSynced); !ok { - return obj - } - switch v := obj.(type) { case *v1.ServiceList: var svcNew []v1.Service @@ -179,16 +94,7 @@ func (nif *nodePortIsolationFilter) Filter(obj runtime.Object, stopCh <-chan str } func (nif *nodePortIsolationFilter) isolateNodePortService(svc *v1.Service) *v1.Service { - nodePoolName := nif.nodePoolName - if len(nodePoolName) == 0 { - node, err := nif.nodeGetter(nif.nodeName) - if err != nil { - klog.Warningf("skip isolateNodePortService filter, failed to get node(%s), %v", nif.nodeName, err) - return svc - } - nodePoolName = node.Labels[nodepoolv1alpha1.LabelCurrentNodePool] - } - + nodePoolName := nif.resolveNodePoolName() // node is not located in NodePool, keep the NodePort service the same as native K8s if len(nodePoolName) == 0 { return svc @@ -201,7 +107,7 @@ func (nif *nodePortIsolationFilter) isolateNodePortService(svc *v1.Service) *v1. if nodePoolConf.Len() != 0 && isNodePoolEnabled(nodePoolConf, nodePoolName) { return svc } else { - klog.V(2).Infof("nodePort service(%s) is disabled in nodePool(%s) by nodePortIsolationFilter", nsName, nodePoolName) + klog.V(2).Infof("service(%s) is disabled in nodePool(%s) by nodePortIsolationFilter", nsName, nodePoolName) return nil } } @@ -210,6 +116,20 @@ func (nif *nodePortIsolationFilter) isolateNodePortService(svc *v1.Service) *v1. return svc } +func (nif *nodePortIsolationFilter) resolveNodePoolName() string { + if len(nif.nodePoolName) != 0 { + return nif.nodePoolName + } + + node, err := nif.client.CoreV1().Nodes().Get(context.Background(), nif.nodeName, metav1.GetOptions{}) + if err != nil { + klog.Warningf("skip isolateNodePortService filter, failed to get node(%s), %v", nif.nodeName, err) + return nif.nodePoolName + } + nif.nodePoolName = node.Labels[apps.LabelDesiredNodePool] + return nif.nodePoolName +} + func getNodePoolConfiguration(v string) sets.String { nodePoolConf := sets.NewString() nodePoolsForValidation := sets.NewString() diff --git a/pkg/yurthub/filter/nodeportisolation/filter_test.go b/pkg/yurthub/filter/nodeportisolation/filter_test.go index 8374ab230a6..871a0e33e15 100644 --- a/pkg/yurthub/filter/nodeportisolation/filter_test.go +++ b/pkg/yurthub/filter/nodeportisolation/filter_test.go @@ -24,14 +24,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" + "github.com/openyurtio/openyurt/pkg/apis/apps" "github.com/openyurtio/openyurt/pkg/util" - "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/filter" - "github.com/openyurtio/openyurt/pkg/yurthub/storage/disk" - hubutil "github.com/openyurtio/openyurt/pkg/yurthub/util" ) func TestName(t *testing.T) { @@ -81,57 +78,37 @@ func TestSetNodeName(t *testing.T) { } } -func TestSetWorkingMode(t *testing.T) { - nif := &nodePortIsolationFilter{} - if err := nif.SetWorkingMode(hubutil.WorkingMode("cloud")); err != nil { - t.Errorf("expect nil, but got %v", err) - } - - if nif.workingMode != hubutil.WorkingModeCloud { - t.Errorf("expect working mode: cloud, but got %s", nif.workingMode) - } -} - -func TestSetSharedInformerFactory(t *testing.T) { +func TestSetKubeClient(t *testing.T) { client := &fake.Clientset{} - informerFactory := informers.NewSharedInformerFactory(client, 0) - nif := &nodePortIsolationFilter{ - workingMode: "cloud", - } - if err := nif.SetSharedInformerFactory(informerFactory); err != nil { - t.Errorf("expect nil, but got %v", err) - } -} - -func TestSetStorageWrapper(t *testing.T) { - nif := &nodePortIsolationFilter{ - workingMode: "edge", - nodeName: "foo", - } - storageManager, err := disk.NewDiskStorage("/tmp/nif-filter") - if err != nil { - t.Fatalf("could not create storage manager, %v", err) - } - storageWrapper := cachemanager.NewStorageWrapper(storageManager) - - if err := nif.SetStorageWrapper(storageWrapper); err != nil { + nif := &nodePortIsolationFilter{} + if err := nif.SetKubeClient(client); err != nil { t.Errorf("expect nil, but got %v", err) } } func TestFilter(t *testing.T) { nodePoolName := "foo" - node := &corev1.Node{ + nodeFoo := &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", + Annotations: map[string]string{ + apps.LabelDesiredNodePool: nodePoolName, + }, + }, + } + nodeBar := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", }, } testcases := map[string]struct { - isOrphanNodes bool - responseObj runtime.Object - expectObj runtime.Object + poolName string + nodeName string + responseObj runtime.Object + expectObj runtime.Object }{ "enable NodePort service listening on nodes in foo and bar NodePool.": { + poolName: nodePoolName, responseObj: &corev1.ServiceList{ Items: []corev1.Service{ { @@ -188,6 +165,7 @@ func TestFilter(t *testing.T) { }, }, "enable NodePort service listening on nodes of all NodePools": { + nodeName: "foo", responseObj: &corev1.ServiceList{ Items: []corev1.Service{ { @@ -244,6 +222,7 @@ func TestFilter(t *testing.T) { }, }, "disable NodePort service listening on nodes of all NodePools": { + poolName: nodePoolName, responseObj: &corev1.ServiceList{ Items: []corev1.Service{ { @@ -277,6 +256,7 @@ func TestFilter(t *testing.T) { expectObj: &corev1.ServiceList{}, }, "disable NodePort service listening only on nodes in foo NodePool": { + poolName: nodePoolName, responseObj: &corev1.ServiceList{ Items: []corev1.Service{ { @@ -320,6 +300,7 @@ func TestFilter(t *testing.T) { }, }, "disable nodeport service": { + poolName: nodePoolName, responseObj: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc1", @@ -336,6 +317,7 @@ func TestFilter(t *testing.T) { expectObj: nil, }, "duplicated node pool configuration": { + nodeName: "foo", responseObj: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc1", @@ -364,6 +346,7 @@ func TestFilter(t *testing.T) { }, }, "disable NodePort service listening on nodes of foo NodePool": { + poolName: nodePoolName, responseObj: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc1", @@ -380,7 +363,7 @@ func TestFilter(t *testing.T) { expectObj: nil, }, "enable nodeport service on orphan nodes": { - isOrphanNodes: true, + nodeName: "bar", responseObj: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "svc1", @@ -409,6 +392,7 @@ func TestFilter(t *testing.T) { }, }, "disable NodePort service listening if no value configured": { + poolName: nodePoolName, responseObj: &corev1.ServiceList{ Items: []corev1.Service{ { @@ -442,6 +426,7 @@ func TestFilter(t *testing.T) { expectObj: &corev1.ServiceList{}, }, "skip podList": { + poolName: nodePoolName, responseObj: &corev1.PodList{ Items: []corev1.Pod{ { @@ -486,17 +471,16 @@ func TestFilter(t *testing.T) { for k, tc := range testcases { t.Run(k, func(t *testing.T) { nif := &nodePortIsolationFilter{} - if !tc.isOrphanNodes { - nif.nodePoolName = nodePoolName - } else { - nif.nodeName = "foo" - nif.nodeGetter = func(name string) (*corev1.Node, error) { - return node, nil - } + if len(tc.poolName) != 0 { + nif.nodePoolName = tc.poolName } - nif.nodeSynced = func() bool { - return true + + if len(tc.nodeName) != 0 { + nif.nodeName = tc.nodeName + client := fake.NewSimpleClientset(nodeFoo, nodeBar) + nif.client = client } + newObj := nif.Filter(tc.responseObj, stopCh) if tc.expectObj == nil { if !util.IsNil(newObj) { diff --git a/pkg/yurthub/filter/servicetopology/filter.go b/pkg/yurthub/filter/servicetopology/filter.go index d25a3a8dd96..f791ad7cfdb 100644 --- a/pkg/yurthub/filter/servicetopology/filter.go +++ b/pkg/yurthub/filter/servicetopology/filter.go @@ -17,23 +17,21 @@ limitations under the License. package servicetopology import ( - "fmt" + "context" v1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" discoveryV1beta1 "k8s.io/api/discovery/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" listers "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/filter" - "github.com/openyurtio/openyurt/pkg/yurthub/storage" - "github.com/openyurtio/openyurt/pkg/yurthub/storage/disk" - "github.com/openyurtio/openyurt/pkg/yurthub/util" nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" appslisters "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/listers/apps/v1alpha1" @@ -58,9 +56,7 @@ func Register(filters *filter.Filters) { } func NewFilter() *serviceTopologyFilter { - return &serviceTopologyFilter{ - workingMode: util.WorkingModeEdge, - } + return &serviceTopologyFilter{} } type serviceTopologyFilter struct { @@ -68,10 +64,9 @@ type serviceTopologyFilter struct { serviceSynced cache.InformerSynced nodePoolLister appslisters.NodePoolLister nodePoolSynced cache.InformerSynced - nodeGetter filter.NodeGetter - nodeSynced cache.InformerSynced + nodePoolName string nodeName string - workingMode util.WorkingMode + client kubernetes.Interface } func (stf *serviceTopologyFilter) Name() string { @@ -85,21 +80,10 @@ func (stf *serviceTopologyFilter) SupportedResourceAndVerbs() map[string]sets.St } } -func (stf *serviceTopologyFilter) SetWorkingMode(mode util.WorkingMode) error { - stf.workingMode = mode - return nil -} - func (stf *serviceTopologyFilter) SetSharedInformerFactory(factory informers.SharedInformerFactory) error { stf.serviceLister = factory.Core().V1().Services().Lister() stf.serviceSynced = factory.Core().V1().Services().Informer().HasSynced - if stf.workingMode == util.WorkingModeCloud { - klog.Infof("prepare list/watch to sync node(%s) for cloud working mode", stf.nodeName) - stf.nodeSynced = factory.Core().V1().Nodes().Informer().HasSynced - stf.nodeGetter = factory.Core().V1().Nodes().Lister().Get - } - return nil } @@ -116,76 +100,32 @@ func (stf *serviceTopologyFilter) SetNodeName(nodeName string) error { return nil } -// TODO: should use disk storage as parameter instead of StorageWrapper -// we can internally construct a new StorageWrapper with passed-in disk storage -func (stf *serviceTopologyFilter) SetStorageWrapper(s cachemanager.StorageWrapper) error { - if s.Name() != disk.StorageName { - return fmt.Errorf("serviceTopologyFilter can only support disk storage currently, cannot use %s", s.Name()) - } - - if len(stf.nodeName) == 0 { - return fmt.Errorf("node name for serviceTopologyFilter is not ready") - } - - // hub agent will list/watch node from kube-apiserver when hub agent work as cloud mode - if stf.workingMode == util.WorkingModeCloud { - return nil - } - klog.Infof("prepare local disk storage to sync node(%s) for edge working mode", stf.nodeName) - - nodeKey, err := s.KeyFunc(storage.KeyBuildInfo{ - Component: "kubelet", - Name: stf.nodeName, - Resources: "nodes", - Group: "", - Version: "v1", - }) - if err != nil { - return fmt.Errorf("failed to get node key for %s, %v", stf.nodeName, err) - } - stf.nodeSynced = func() bool { - obj, err := s.Get(nodeKey) - if err != nil || obj == nil { - return false - } +func (stf *serviceTopologyFilter) SetNodePoolName(poolName string) error { + stf.nodePoolName = poolName + return nil +} - if _, ok := obj.(*v1.Node); !ok { - return false - } +func (stf *serviceTopologyFilter) SetKubeClient(client kubernetes.Interface) error { + stf.client = client + return nil +} - return true +func (stf *serviceTopologyFilter) resolveNodePoolName() string { + if len(stf.nodePoolName) != 0 { + return stf.nodePoolName } - stf.nodeGetter = func(name string) (*v1.Node, error) { - key, err := s.KeyFunc(storage.KeyBuildInfo{ - Component: "kubelet", - Name: name, - Resources: "nodes", - Group: "", - Version: "v1", - }) - if err != nil { - return nil, fmt.Errorf("nodeGetter failed to get node key for %s, %v", name, err) - } - obj, err := s.Get(key) - if err != nil { - return nil, err - } else if obj == nil { - return nil, fmt.Errorf("node(%s) is not ready", name) - } - - if node, ok := obj.(*v1.Node); ok { - return node, nil - } - - return nil, fmt.Errorf("node(%s) is not found", name) + node, err := stf.client.CoreV1().Nodes().Get(context.Background(), stf.nodeName, metav1.GetOptions{}) + if err != nil { + klog.Warningf("failed to get node(%s) in serviceTopologyFilter filter, %v", stf.nodeName, err) + return stf.nodePoolName } - - return nil + stf.nodePoolName = node.Labels[nodepoolv1alpha1.LabelDesiredNodePool] + return stf.nodePoolName } func (stf *serviceTopologyFilter) Filter(obj runtime.Object, stopCh <-chan struct{}) runtime.Object { - if ok := cache.WaitForCacheSync(stopCh, stf.nodeSynced, stf.serviceSynced, stf.nodePoolSynced); !ok { + if ok := cache.WaitForCacheSync(stopCh, stf.serviceSynced, stf.nodePoolSynced); !ok { return obj } @@ -282,14 +222,8 @@ func (stf *serviceTopologyFilter) nodeTopologyHandler(obj runtime.Object) runtim } func (stf *serviceTopologyFilter) nodePoolTopologyHandler(obj runtime.Object) runtime.Object { - currentNode, err := stf.nodeGetter(stf.nodeName) - if err != nil { - klog.Warningf("skip serviceTopologyFilterHandler, failed to get current node %s, err: %v", stf.nodeName, err) - return obj - } - - nodePoolName, ok := currentNode.Labels[nodepoolv1alpha1.LabelCurrentNodePool] - if !ok || len(nodePoolName) == 0 { + nodePoolName := stf.resolveNodePoolName() + if len(nodePoolName) == 0 { klog.Infof("node(%s) is not added into node pool, so fall into node topology", stf.nodeName) return stf.nodeTopologyHandler(obj) } diff --git a/pkg/yurthub/filter/servicetopology/filter_test.go b/pkg/yurthub/filter/servicetopology/filter_test.go index 2f62ecab639..b86e7c5d074 100644 --- a/pkg/yurthub/filter/servicetopology/filter_test.go +++ b/pkg/yurthub/filter/servicetopology/filter_test.go @@ -17,7 +17,6 @@ limitations under the License. package servicetopology import ( - "context" "reflect" "testing" "time" @@ -69,12 +68,15 @@ func TestFilter(t *testing.T) { nodeName3 := "node3" testcases := map[string]struct { + poolName string + nodeName string responseObject runtime.Object kubeClient *k8sfake.Clientset yurtClient *yurtfake.Clientset expectObject runtime.Object }{ "v1beta1.EndpointSliceList: topologyKeys is kubernetes.io/hostname": { + poolName: "hangzhou", responseObject: &discoveryV1beta1.EndpointSliceList{ Items: []discoveryV1beta1.EndpointSlice{ { @@ -127,7 +129,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -135,7 +137,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -143,7 +145,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -219,6 +221,7 @@ func TestFilter(t *testing.T) { }, }, "v1beta1.EndpointSliceList: topologyKeys is openyurt.io/nodepool": { + poolName: "hangzhou", responseObject: &discoveryV1beta1.EndpointSliceList{ Items: []discoveryV1beta1.EndpointSlice{ { @@ -271,7 +274,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -279,7 +282,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -287,7 +290,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -423,7 +426,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -431,7 +434,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -439,7 +442,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -575,7 +578,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -583,7 +586,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -591,7 +594,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -739,7 +742,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -747,7 +750,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -874,7 +877,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -882,7 +885,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -890,7 +893,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1000,7 +1003,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -1008,7 +1011,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1016,7 +1019,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1123,7 +1126,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -1131,7 +1134,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1139,7 +1142,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1272,7 +1275,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1280,7 +1283,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -1288,7 +1291,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1404,7 +1407,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1412,7 +1415,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -1420,7 +1423,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1542,7 +1545,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1550,7 +1553,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -1558,7 +1561,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1680,7 +1683,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1688,7 +1691,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -1696,7 +1699,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1828,7 +1831,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -1836,7 +1839,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1939,7 +1942,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -1947,7 +1950,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -1955,7 +1958,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2045,7 +2048,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2053,7 +2056,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2061,7 +2064,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2160,7 +2163,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2168,7 +2171,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2176,7 +2179,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2290,7 +2293,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2298,7 +2301,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2306,7 +2309,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2424,7 +2427,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2432,7 +2435,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2440,7 +2443,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2558,7 +2561,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2566,7 +2569,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2574,7 +2577,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2700,7 +2703,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2708,7 +2711,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2813,7 +2816,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2821,7 +2824,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2829,7 +2832,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2921,7 +2924,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -2929,7 +2932,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -2937,7 +2940,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -3029,7 +3032,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -3037,7 +3040,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -3045,7 +3048,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -3129,7 +3132,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "hangzhou", + nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", }, }, }, @@ -3137,7 +3140,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -3145,7 +3148,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelCurrentNodePool: "shanghai", + nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", }, }, }, @@ -3220,24 +3223,22 @@ func TestFilter(t *testing.T) { yurtFactory.Start(stopper2) yurtFactory.WaitForCacheSync(stopper2) - nodeGetter := func(name string) (*corev1.Node, error) { - return tt.kubeClient.CoreV1().Nodes().Get(context.TODO(), name, metav1.GetOptions{}) - } - - nodeSynced := func() bool { - return true - } - stopCh := make(<-chan struct{}) stf := &serviceTopologyFilter{ nodeName: currentNodeName, serviceLister: serviceLister, nodePoolLister: nodePoolLister, - nodeGetter: nodeGetter, serviceSynced: serviceSynced, nodePoolSynced: nodePoolSynced, - nodeSynced: nodeSynced, + client: tt.kubeClient, } + + if len(tt.poolName) != 0 { + stf.nodePoolName = tt.poolName + } else { + stf.nodeName = currentNodeName + } + newObj := stf.Filter(tt.responseObject, stopCh) if util.IsNil(newObj) { t.Errorf("empty object is returned") From b2d7d49e2d81cd44da2d7e528932f000d7ffae2e Mon Sep 17 00:00:00 2001 From: Regina <52201043+Rui-Gan@users.noreply.github.com> Date: Mon, 19 Jun 2023 10:25:12 +0800 Subject: [PATCH 35/93] move iot controller to yurt-manager (#1488) * add a new crd device/v1alpha1&v1alpha2/iot and controller, webhook Signed-off-by: Rui-Gan <1171530954@qq.com> add a new crd device/v1alpha1&v1alpha2/iot and controller, webhook Signed-off-by: Rui-Gan <1171530954@qq.com> * refactor: rename IoT crd and device group to PlatformAdmin crd and iot group Signed-off-by: LavenderQAQ <1254297317@qq.com> --------- Signed-off-by: Rui-Gan <1171530954@qq.com> Signed-off-by: LavenderQAQ <1254297317@qq.com> Co-authored-by: LavenderQAQ <1254297317@qq.com> --- .../crds/iot.openyurt.io_platformadmins.yaml | 8493 +++++++++++++++++ .../yurt-manager-auto-generated.yaml | 88 + cmd/yurt-manager/app/options/options.go | 7 + .../app/options/platformadmincontroller.go | 63 + go.mod | 2 +- pkg/apis/addtoscheme_iot_v1alpha1.go | 26 + pkg/apis/addtoscheme_iot_v1alpha2.go | 26 + pkg/apis/iot/v1alpha1/condition_const.go | 38 + pkg/apis/iot/v1alpha1/default.go | 24 + pkg/apis/iot/v1alpha1/doc.go | 17 + pkg/apis/iot/v1alpha1/groupversion_info.go | 44 + .../iot/v1alpha1/platformadmin_conversion.go | 141 + pkg/apis/iot/v1alpha1/platformadmin_types.go | 142 + .../iot/v1alpha1/zz_generated.deepcopy.go | 186 + pkg/apis/iot/v1alpha2/condition_const.go | 32 + pkg/apis/iot/v1alpha2/default.go | 24 + pkg/apis/iot/v1alpha2/doc.go | 17 + pkg/apis/iot/v1alpha2/groupversion_info.go | 44 + .../iot/v1alpha2/platformadmin_conversion.go | 20 + pkg/apis/iot/v1alpha2/platformadmin_types.go | 141 + .../iot/v1alpha2/zz_generated.deepcopy.go | 158 + pkg/controller/apis/config/types.go | 4 + pkg/controller/controller.go | 2 + .../config/EdgeXConfig/config-nosecty.json | 4657 +++++++++ .../config/EdgeXConfig/config.json | 1 + .../config/EdgeXConfig/manifest.yaml | 9 + pkg/controller/platformadmin/config/types.go | 107 + .../platformadmin/platformadmin_controller.go | 602 ++ .../platformadmin/utils/fieldindexer.go | 50 + pkg/controller/platformadmin/utils/util.go | 72 + .../v1alpha1/platformadmin_default.go | 43 + .../v1alpha1/platformadmin_handler.go | 86 + .../v1alpha1/platformadmin_validation.go | 118 + .../v1alpha2/platformadmin_default.go | 47 + .../v1alpha2/platformadmin_handler.go | 89 + .../v1alpha2/platformadmin_validation.go | 144 + pkg/webhook/server.go | 5 + 37 files changed, 15768 insertions(+), 1 deletion(-) create mode 100644 charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml create mode 100644 cmd/yurt-manager/app/options/platformadmincontroller.go create mode 100644 pkg/apis/addtoscheme_iot_v1alpha1.go create mode 100644 pkg/apis/addtoscheme_iot_v1alpha2.go create mode 100644 pkg/apis/iot/v1alpha1/condition_const.go create mode 100644 pkg/apis/iot/v1alpha1/default.go create mode 100644 pkg/apis/iot/v1alpha1/doc.go create mode 100644 pkg/apis/iot/v1alpha1/groupversion_info.go create mode 100644 pkg/apis/iot/v1alpha1/platformadmin_conversion.go create mode 100644 pkg/apis/iot/v1alpha1/platformadmin_types.go create mode 100644 pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go create mode 100644 pkg/apis/iot/v1alpha2/condition_const.go create mode 100644 pkg/apis/iot/v1alpha2/default.go create mode 100644 pkg/apis/iot/v1alpha2/doc.go create mode 100644 pkg/apis/iot/v1alpha2/groupversion_info.go create mode 100644 pkg/apis/iot/v1alpha2/platformadmin_conversion.go create mode 100644 pkg/apis/iot/v1alpha2/platformadmin_types.go create mode 100644 pkg/apis/iot/v1alpha2/zz_generated.deepcopy.go create mode 100644 pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json create mode 100644 pkg/controller/platformadmin/config/EdgeXConfig/config.json create mode 100644 pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml create mode 100644 pkg/controller/platformadmin/config/types.go create mode 100644 pkg/controller/platformadmin/platformadmin_controller.go create mode 100644 pkg/controller/platformadmin/utils/fieldindexer.go create mode 100644 pkg/controller/platformadmin/utils/util.go create mode 100644 pkg/webhook/platformadmin/v1alpha1/platformadmin_default.go create mode 100644 pkg/webhook/platformadmin/v1alpha1/platformadmin_handler.go create mode 100644 pkg/webhook/platformadmin/v1alpha1/platformadmin_validation.go create mode 100644 pkg/webhook/platformadmin/v1alpha2/platformadmin_default.go create mode 100644 pkg/webhook/platformadmin/v1alpha2/platformadmin_handler.go create mode 100644 pkg/webhook/platformadmin/v1alpha2/platformadmin_validation.go diff --git a/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml b/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml new file mode 100644 index 00000000000..9b71d04091b --- /dev/null +++ b/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml @@ -0,0 +1,8493 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: platformadmins.iot.openyurt.io +spec: + group: iot.openyurt.io + names: + categories: + - all + kind: PlatformAdmin + listKind: PlatformAdminList + plural: platformadmins + shortNames: + - pa + singular: platformadmin + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The platform ready status + jsonPath: .status.ready + name: READY + type: boolean + - description: The Service Replica. + jsonPath: .status.serviceReplicas + name: Service + type: integer + - description: The Ready Service Replica. + jsonPath: .status.serviceReadyReplicas + name: ReadyService + type: integer + - description: The Deployment Replica. + jsonPath: .status.deploymentReplicas + name: Deployment + type: integer + - description: The Ready Deployment Replica. + jsonPath: .status.deploymentReadyReplicas + name: ReadyDeployment + type: integer + deprecated: true + deprecationWarning: iot.openyurt.io/v1alpha1 PlatformAdmin will be deprecated + in future; use iot.openyurt.io/v1alpha2 PlatformAdmin; v1alpha1 PlatformAdmin.Spec.ServiceType + only support ClusterIP + name: v1alpha1 + schema: + openAPIV3Schema: + description: PlatformAdmin is the Schema for the samples API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PlatformAdminSpec defines the desired state of PlatformAdmin + properties: + additionalDeployments: + items: + description: DeploymentTemplateSpec defines the pool template of + Deployment. + properties: + metadata: + type: object + spec: + description: DeploymentSpec is the specification of the desired + behavior of the Deployment. + properties: + minReadySeconds: + description: Minimum number of seconds for which a newly + created pod should be ready without any of its container + crashing, for it to be considered available. Defaults + to 0 (pod will be considered available as soon as it is + ready) + format: int32 + type: integer + paused: + description: Indicates that the deployment is paused. + type: boolean + progressDeadlineSeconds: + description: The maximum time in seconds for a deployment + to make progress before it is considered to be failed. + The deployment controller will continue to process failed + deployments and a condition with a ProgressDeadlineExceeded + reason will be surfaced in the deployment status. Note + that progress will not be estimated during the time a + deployment is paused. Defaults to 600s. + format: int32 + type: integer + replicas: + description: Number of desired pods. This is a pointer to + distinguish between explicit zero and not specified. Defaults + to 1. + format: int32 + type: integer + revisionHistoryLimit: + description: The number of old ReplicaSets to retain to + allow rollback. This is a pointer to distinguish between + explicit zero and not specified. Defaults to 10. + format: int32 + type: integer + selector: + description: Label selector for pods. Existing ReplicaSets + whose pods are selected by this will be the ones affected + by this deployment. It must match the pod template's labels. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + strategy: + description: The deployment strategy to use to replace existing + pods with new ones. + properties: + rollingUpdate: + description: 'Rolling update config params. Present + only if DeploymentStrategyType = RollingUpdate. --- + TODO: Update this to follow our convention for oneOf, + whatever we decide it to be.' + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be scheduled above the desired number of pods. + Value can be an absolute number (ex: 5) or a percentage + of desired pods (ex: 10%). This can not be 0 if + MaxUnavailable is 0. Absolute number is calculated + from percentage by rounding up. Defaults to 25%. + Example: when this is set to 30%, the new ReplicaSet + can be scaled up immediately when the rolling + update starts, such that the total number of old + and new pods do not exceed 130% of desired pods. + Once old pods have been killed, new ReplicaSet + can be scaled up further, ensuring that total + number of pods running at any time during the + update is at most 130% of desired pods.' + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding down. This can not + be 0 if MaxSurge is 0. Defaults to 25%. Example: + when this is set to 30%, the old ReplicaSet can + be scaled down to 70% of desired pods immediately + when the rolling update starts. Once new pods + are ready, old ReplicaSet can be scaled down further, + followed by scaling up the new ReplicaSet, ensuring + that the total number of pods available at all + times during the update is at least 70% of desired + pods.' + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or + "RollingUpdate". Default is RollingUpdate. + type: string + type: object + template: + description: Template describes the pods that will be created. + properties: + metadata: + description: 'Standard object''s metadata. More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + type: object + spec: + description: 'Specification of the desired behavior + of the pod. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + activeDeadlineSeconds: + description: Optional duration in seconds the pod + may be active on the node relative to StartTime + before the system will actively try to mark it + failed and kill associated containers. Value must + be a positive integer. + format: int64 + type: integer + affinity: + description: If specified, the pod's scheduling + constraints + properties: + nodeAffinity: + description: Describes node affinity scheduling + rules for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to + schedule pods to nodes that satisfy the + affinity expressions specified by this + field, but it may choose a node that violates + one or more of the expressions. The node + that is most preferred is the one with + the greatest sum of weights, i.e. for + each node that meets all of the scheduling + requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a + sum by iterating through the elements + of this field and adding "weight" to the + sum if the node matches the corresponding + matchExpressions; the node(s) with the + highest sum are the most preferred. + items: + description: An empty preferred scheduling + term matches all objects with implicit + weight 0 (i.e. it's a no-op). A null + preferred scheduling term matches no + objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, + associated with the corresponding + weight. + properties: + matchExpressions: + description: A list of node selector + requirements by node's labels. + items: + description: A node selector + requirement is a selector + that contains values, a key, + and an operator that relates + the key and values. + properties: + key: + description: The label key + that the selector applies + to. + type: string + operator: + description: Represents + a key's relationship to + a set of values. Valid + operators are In, NotIn, + Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of + string values. If the + operator is In or NotIn, + the values array must + be non-empty. If the operator + is Exists or DoesNotExist, + the values array must + be empty. If the operator + is Gt or Lt, the values + array must have a single + element, which will be + interpreted as an integer. + This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector + requirements by node's fields. + items: + description: A node selector + requirement is a selector + that contains values, a key, + and an operator that relates + the key and values. + properties: + key: + description: The label key + that the selector applies + to. + type: string + operator: + description: Represents + a key's relationship to + a set of values. Valid + operators are In, NotIn, + Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of + string values. If the + operator is In or NotIn, + the values array must + be non-empty. If the operator + is Exists or DoesNotExist, + the values array must + be empty. If the operator + is Gt or Lt, the values + array must have a single + element, which will be + interpreted as an integer. + This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with + matching the corresponding nodeSelectorTerm, + in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements + specified by this field are not met at + scheduling time, the pod will not be scheduled + onto the node. If the affinity requirements + specified by this field cease to be met + at some point during pod execution (e.g. + due to an update), the system may or may + not try to eventually evict the pod from + its node. + properties: + nodeSelectorTerms: + description: Required. A list of node + selector terms. The terms are ORed. + items: + description: A null or empty node + selector term matches no objects. + The requirements of them are ANDed. + The TopologySelectorTerm type implements + a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector + requirements by node's labels. + items: + description: A node selector + requirement is a selector + that contains values, a key, + and an operator that relates + the key and values. + properties: + key: + description: The label key + that the selector applies + to. + type: string + operator: + description: Represents + a key's relationship to + a set of values. Valid + operators are In, NotIn, + Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of + string values. If the + operator is In or NotIn, + the values array must + be non-empty. If the operator + is Exists or DoesNotExist, + the values array must + be empty. If the operator + is Gt or Lt, the values + array must have a single + element, which will be + interpreted as an integer. + This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector + requirements by node's fields. + items: + description: A node selector + requirement is a selector + that contains values, a key, + and an operator that relates + the key and values. + properties: + key: + description: The label key + that the selector applies + to. + type: string + operator: + description: Represents + a key's relationship to + a set of values. Valid + operators are In, NotIn, + Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of + string values. If the + operator is In or NotIn, + the values array must + be non-empty. If the operator + is Exists or DoesNotExist, + the values array must + be empty. If the operator + is Gt or Lt, the values + array must have a single + element, which will be + interpreted as an integer. + This array is replaced + during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling + rules (e.g. co-locate this pod in the same + node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to + schedule pods to nodes that satisfy the + affinity expressions specified by this + field, but it may choose a node that violates + one or more of the expressions. The node + that is most preferred is the one with + the greatest sum of weights, i.e. for + each node that meets all of the scheduling + requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a + sum by iterating through the elements + of this field and adding "weight" to the + sum if the node has pods which matches + the corresponding podAffinityTerm; the + node(s) with the highest sum are the most + preferred. + items: + description: The weights of all of the + matched WeightedPodAffinityTerm fields + are added per-node to find the most + preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity + term, associated with the corresponding + weight. + properties: + labelSelector: + description: A label query over + a set of resources, in this + case pods. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, + a key, and an operator + that relates the key and + values. + properties: + key: + description: key is + the label key that + the selector applies + to. + type: string + operator: + description: operator + represents a key's + relationship to a + set of values. Valid + operators are In, + NotIn, Exists and + DoesNotExist. + type: string + values: + description: values + is an array of string + values. If the operator + is In or NotIn, the + values array must + be non-empty. If the + operator is Exists + or DoesNotExist, the + values array must + be empty. This array + is replaced during + a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is + a map of {key,value} pairs. + A single {key,value} in + the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", + the operator is "In", and + the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + namespaceSelector: + description: A label query over + the set of namespaces that the + term applies to. The term is + applied to the union of the + namespaces selected by this + field and the ones listed in + the namespaces field. null selector + and null or empty namespaces + list means "this pod's namespace". + An empty selector ({}) matches + all namespaces. This field is + beta-level and is only honored + when PodAffinityNamespaceSelector + feature is enabled. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, + a key, and an operator + that relates the key and + values. + properties: + key: + description: key is + the label key that + the selector applies + to. + type: string + operator: + description: operator + represents a key's + relationship to a + set of values. Valid + operators are In, + NotIn, Exists and + DoesNotExist. + type: string + values: + description: values + is an array of string + values. If the operator + is In or NotIn, the + values array must + be non-empty. If the + operator is Exists + or DoesNotExist, the + values array must + be empty. This array + is replaced during + a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is + a map of {key,value} pairs. + A single {key,value} in + the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", + the operator is "In", and + the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies + a static list of namespace names + that the term applies to. The + term is applied to the union + of the namespaces listed in + this field and the ones selected + by namespaceSelector. null or + empty namespaces list and null + namespaceSelector means "this + pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be + co-located (affinity) or not + co-located (anti-affinity) with + the pods matching the labelSelector + in the specified namespaces, + where co-located is defined + as running on a node whose value + of the label with key topologyKey + matches that of any node on + which any of the selected pods + is running. Empty topologyKey + is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with + matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements + specified by this field are not met at + scheduling time, the pod will not be scheduled + onto the node. If the affinity requirements + specified by this field cease to be met + at some point during pod execution (e.g. + due to a pod label update), the system + may or may not try to eventually evict + the pod from its node. When there are + multiple elements, the lists of nodes + corresponding to each podAffinityTerm + are intersected, i.e. all terms must be + satisfied. + items: + description: Defines a set of pods (namely + those matching the labelSelector relative + to the given namespace(s)) that this + pod should be co-located (affinity) + or not co-located (anti-affinity) with, + where co-located is defined as running + on a node whose value of the label with + key matches that of any + node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a + set of resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, a key, + and an operator that relates + the key and values. + properties: + key: + description: key is the + label key that the selector + applies to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid + operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In + or NotIn, the values array + must be non-empty. If + the operator is Exists + or DoesNotExist, the values + array must be empty. This + array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a + map of {key,value} pairs. A + single {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator + is "In", and the values array + contains only "value". The requirements + are ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the + set of namespaces that the term + applies to. The term is applied + to the union of the namespaces selected + by this field and the ones listed + in the namespaces field. null selector + and null or empty namespaces list + means "this pod's namespace". An + empty selector ({}) matches all + namespaces. This field is beta-level + and is only honored when PodAffinityNamespaceSelector + feature is enabled. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, a key, + and an operator that relates + the key and values. + properties: + key: + description: key is the + label key that the selector + applies to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid + operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In + or NotIn, the values array + must be non-empty. If + the operator is Exists + or DoesNotExist, the values + array must be empty. This + array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a + map of {key,value} pairs. A + single {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator + is "In", and the values array + contains only "value". The requirements + are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies + a static list of namespace names + that the term applies to. The term + is applied to the union of the namespaces + listed in this field and the ones + selected by namespaceSelector. null + or empty namespaces list and null + namespaceSelector means "this pod's + namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where + co-located is defined as running + on a node whose value of the label + with key topologyKey matches that + of any node on which any of the + selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling + rules (e.g. avoid putting this pod in the + same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to + schedule pods to nodes that satisfy the + anti-affinity expressions specified by + this field, but it may choose a node that + violates one or more of the expressions. + The node that is most preferred is the + one with the greatest sum of weights, + i.e. for each node that meets all of the + scheduling requirements (resource request, + requiredDuringScheduling anti-affinity + expressions, etc.), compute a sum by iterating + through the elements of this field and + adding "weight" to the sum if the node + has pods which matches the corresponding + podAffinityTerm; the node(s) with the + highest sum are the most preferred. + items: + description: The weights of all of the + matched WeightedPodAffinityTerm fields + are added per-node to find the most + preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity + term, associated with the corresponding + weight. + properties: + labelSelector: + description: A label query over + a set of resources, in this + case pods. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, + a key, and an operator + that relates the key and + values. + properties: + key: + description: key is + the label key that + the selector applies + to. + type: string + operator: + description: operator + represents a key's + relationship to a + set of values. Valid + operators are In, + NotIn, Exists and + DoesNotExist. + type: string + values: + description: values + is an array of string + values. If the operator + is In or NotIn, the + values array must + be non-empty. If the + operator is Exists + or DoesNotExist, the + values array must + be empty. This array + is replaced during + a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is + a map of {key,value} pairs. + A single {key,value} in + the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", + the operator is "In", and + the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + namespaceSelector: + description: A label query over + the set of namespaces that the + term applies to. The term is + applied to the union of the + namespaces selected by this + field and the ones listed in + the namespaces field. null selector + and null or empty namespaces + list means "this pod's namespace". + An empty selector ({}) matches + all namespaces. This field is + beta-level and is only honored + when PodAffinityNamespaceSelector + feature is enabled. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, + a key, and an operator + that relates the key and + values. + properties: + key: + description: key is + the label key that + the selector applies + to. + type: string + operator: + description: operator + represents a key's + relationship to a + set of values. Valid + operators are In, + NotIn, Exists and + DoesNotExist. + type: string + values: + description: values + is an array of string + values. If the operator + is In or NotIn, the + values array must + be non-empty. If the + operator is Exists + or DoesNotExist, the + values array must + be empty. This array + is replaced during + a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is + a map of {key,value} pairs. + A single {key,value} in + the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", + the operator is "In", and + the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies + a static list of namespace names + that the term applies to. The + term is applied to the union + of the namespaces listed in + this field and the ones selected + by namespaceSelector. null or + empty namespaces list and null + namespaceSelector means "this + pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be + co-located (affinity) or not + co-located (anti-affinity) with + the pods matching the labelSelector + in the specified namespaces, + where co-located is defined + as running on a node whose value + of the label with key topologyKey + matches that of any node on + which any of the selected pods + is running. Empty topologyKey + is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with + matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements + specified by this field are not met at + scheduling time, the pod will not be scheduled + onto the node. If the anti-affinity requirements + specified by this field cease to be met + at some point during pod execution (e.g. + due to a pod label update), the system + may or may not try to eventually evict + the pod from its node. When there are + multiple elements, the lists of nodes + corresponding to each podAffinityTerm + are intersected, i.e. all terms must be + satisfied. + items: + description: Defines a set of pods (namely + those matching the labelSelector relative + to the given namespace(s)) that this + pod should be co-located (affinity) + or not co-located (anti-affinity) with, + where co-located is defined as running + on a node whose value of the label with + key matches that of any + node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a + set of resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, a key, + and an operator that relates + the key and values. + properties: + key: + description: key is the + label key that the selector + applies to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid + operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In + or NotIn, the values array + must be non-empty. If + the operator is Exists + or DoesNotExist, the values + array must be empty. This + array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a + map of {key,value} pairs. A + single {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator + is "In", and the values array + contains only "value". The requirements + are ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the + set of namespaces that the term + applies to. The term is applied + to the union of the namespaces selected + by this field and the ones listed + in the namespaces field. null selector + and null or empty namespaces list + means "this pod's namespace". An + empty selector ({}) matches all + namespaces. This field is beta-level + and is only honored when PodAffinityNamespaceSelector + feature is enabled. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, a key, + and an operator that relates + the key and values. + properties: + key: + description: key is the + label key that the selector + applies to. + type: string + operator: + description: operator represents + a key's relationship to + a set of values. Valid + operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an + array of string values. + If the operator is In + or NotIn, the values array + must be non-empty. If + the operator is Exists + or DoesNotExist, the values + array must be empty. This + array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a + map of {key,value} pairs. A + single {key,value} in the matchLabels + map is equivalent to an element + of matchExpressions, whose key + field is "key", the operator + is "In", and the values array + contains only "value". The requirements + are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies + a static list of namespace names + that the term applies to. The term + is applied to the union of the namespaces + listed in this field and the ones + selected by namespaceSelector. null + or empty namespaces list and null + namespaceSelector means "this pod's + namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where + co-located is defined as running + on a node whose value of the label + with key topologyKey matches that + of any node on which any of the + selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + description: AutomountServiceAccountToken indicates + whether a service account token should be automatically + mounted. + type: boolean + containers: + description: List of containers belonging to the + pod. Containers cannot currently be added or removed. + There must be at least one container in a Pod. + Cannot be updated. + items: + description: A single application container that + you want to run within a pod. + properties: + args: + description: 'Arguments to the entrypoint. + The docker image''s CMD is used if this + is not provided. Variable references $(VAR_NAME) + are expanded using the container''s environment. + If a variable cannot be resolved, the reference + in the input string will be unchanged. Double + $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal + "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable + exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed + within a shell. The docker image''s ENTRYPOINT + is used if this is not provided. Variable + references $(VAR_NAME) are expanded using + the container''s environment. If a variable + cannot be resolved, the reference in the + input string will be unchanged. Double $$ + are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal + "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable + exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables + to set in the container. Cannot be updated. + items: + description: EnvVar represents an environment + variable present in a Container. + properties: + name: + description: Name of the environment + variable. Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously + defined environment variables in the + container and any service environment + variables. If a variable cannot be + resolved, the reference in the input + string will be unchanged. Double $$ + are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: + i.e. "$$(VAR_NAME)" will produce the + string literal "$(VAR_NAME)". Escaped + references will never be expanded, + regardless of whether the variable + exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment + variable's value. Cannot be used if + value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a + ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether + the ConfigMap or its key must + be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of + the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, + `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource + of the container: only resources + limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory + and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a + secret in the pod's namespace + properties: + key: + description: The key of the + secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether + the Secret or its key must + be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined + within a source must be a C_IDENTIFIER. + All invalid keys will be reported as an + event when the container is starting. When + a key exists in multiple sources, the value + associated with the last source will take + precedence. Values defined by an Env with + a duplicate key will take precedence. Cannot + be updated. + items: + description: EnvFromSource represents the + source of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select + from + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the + ConfigMap must be defined + type: boolean + type: object + prefix: + description: An optional identifier + to prepend to each key in the ConfigMap. + Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the + Secret must be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Docker image name. More info: + https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level + config management to default or override + container images in workload controllers + like Deployments and StatefulSets.' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, + Never, IfNotPresent. Defaults to Always + if :latest tag is specified, or IfNotPresent + otherwise. Cannot be updated. More info: + https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Actions that the management system + should take in response to container lifecycle + events. Cannot be updated. + properties: + postStart: + description: 'PostStart is called immediately + after a container is created. If the + handler fails, the container is terminated + and restarted according to its restart + policy. Other management of the container + blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: One and only one of the + following should be specified. Exec + specifies the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the + command is root ('/') in the + container's filesystem. The + command is simply exec'd, it + is not run inside a shell, so + traditional shell instructions + ('|', etc) won't work. To use + a shell, you need to explicitly + call out to that shell. Exit + status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the + http request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. + You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to + set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes + a custom header to be used + in HTTP probes + properties: + name: + description: The header + field name + type: string + value: + description: The header + field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on + the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for + connecting to the host. Defaults + to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: 'TCPSocket specifies + an action involving a TCP port. + TCP hooks not yet supported TODO: + implement a realistic TCP lifecycle + hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the + pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately + before a container is terminated due + to an API request or management event + such as liveness/startup probe failure, + preemption, resource contention, etc. + The handler is not called if the container + crashes or exits. The reason for termination + is passed to the handler. The Pod''s + termination grace period countdown begins + before the PreStop hooked is executed. + Regardless of the outcome of the handler, + the container will eventually terminate + within the Pod''s termination grace + period. Other management of the container + blocks until the hook completes or until + the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: One and only one of the + following should be specified. Exec + specifies the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the + command is root ('/') in the + container's filesystem. The + command is simply exec'd, it + is not run inside a shell, so + traditional shell instructions + ('|', etc) won't work. To use + a shell, you need to explicitly + call out to that shell. Exit + status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the + http request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. + You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to + set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes + a custom header to be used + in HTTP probes + properties: + name: + description: The header + field name + type: string + value: + description: The header + field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on + the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for + connecting to the host. Defaults + to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: 'TCPSocket specifies + an action involving a TCP port. + TCP hooks not yet supported TODO: + implement a realistic TCP lifecycle + hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the + pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: 'Periodic probe of container + liveness. Container will be restarted if + the probe fails. Cannot be updated. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the container specified + as a DNS_LABEL. Each container in a pod + must have a unique name (DNS_LABEL). Cannot + be updated. + type: string + ports: + description: List of ports to expose from + the container. Exposing a port here gives + the system additional information about + the network connections a container uses, + but is primarily informational. Not specifying + a port here DOES NOT prevent that port from + being exposed. Any port which is listening + on the default "0.0.0.0" address inside + a container will be accessible from the + network. Cannot be updated. + items: + description: ContainerPort represents a + network port in a single container. + properties: + containerPort: + description: Number of port to expose + on the pod's IP address. This must + be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the + external port to. + type: string + hostPort: + description: Number of port to expose + on the host. If specified, this must + be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this + must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must + be an IANA_SVC_NAME and unique within + the pod. Each named port in a pod + must have a unique name. Name for + the port that can be referred to by + services. + type: string + protocol: + default: TCP + description: Protocol for port. Must + be UDP, TCP, or SCTP. Defaults to + "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: 'Periodic probe of container + service readiness. Container will be removed + from service endpoints if the probe fails. + Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: 'Compute Resources required by + this container. Cannot be updated. More + info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum + amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum + amount of compute resources required. + If Requests is omitted for a container, + it defaults to Limits if that is explicitly + specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'SecurityContext defines the + security options the container should be + run with. If set, the fields of SecurityContext + override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation + controls whether a process can gain + more privileges than its parent process. + This bool directly controls if the no_new_privs + flag will be set on the container process. + AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged + 2) has CAP_SYS_ADMIN' + type: boolean + capabilities: + description: The capabilities to add/drop + when running containers. Defaults to + the default set of capabilities granted + by the container runtime. + properties: + add: + description: Added capabilities + items: + description: Capability represent + POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent + POSIX capabilities type + type: string + type: array + type: object + privileged: + description: Run container in privileged + mode. Processes in privileged containers + are essentially equivalent to root on + the host. Defaults to false. + type: boolean + procMount: + description: procMount denotes the type + of proc mount to use for the containers. + The default is DefaultProcMount which + uses the container runtime defaults + for readonly paths and masked paths. + This requires the ProcMountType feature + flag to be enabled. + type: string + readOnlyRootFilesystem: + description: Whether this container has + a read-only root filesystem. Default + is false. + type: boolean + runAsGroup: + description: The GID to run the entrypoint + of the container process. Uses runtime + default if unset. May also be set in + PodSecurityContext. If set in both + SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container + must run as a non-root user. If true, + the Kubelet will validate the image + at runtime to ensure that it does not + run as UID 0 (root) and fail to start + the container if it does. If unset or + false, no such validation will be performed. + May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint + of the container process. Defaults to + user specified in image metadata if + unspecified. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be + applied to the container. If unspecified, + the container runtime will allocate + a random SELinux context for each container. May + also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + properties: + level: + description: Level is SELinux level + label that applies to the container. + type: string + role: + description: Role is a SELinux role + label that applies to the container. + type: string + type: + description: Type is a SELinux type + label that applies to the container. + type: string + user: + description: User is a SELinux user + label that applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use + by this container. If seccomp options + are provided at both the pod & container + level, the container options override + the pod options. + properties: + localhostProfile: + description: localhostProfile indicates + a profile defined in a file on the + node should be used. The profile + must be preconfigured on the node + to work. Must be a descending path, + relative to the kubelet's configured + seccomp profile location. Must only + be set if type is "Localhost". + type: string + type: + description: "type indicates which + kind of seccomp profile will be + applied. Valid options are: \n Localhost + - a profile defined in a file on + the node should be used. RuntimeDefault + - the container runtime default + profile should be used. Unconfined + - no profile should be applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings + applied to all containers. If unspecified, + the options from the PodSecurityContext + will be used. If set in both SecurityContext + and PodSecurityContext, the value specified + in SecurityContext takes precedence. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is + where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA + credential spec named by the GMSACredentialSpecName + field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName + is the name of the GMSA credential + spec to use. + type: string + hostProcess: + description: HostProcess determines + if a container should be run as + a 'Host Process' container. This + field is alpha-level and will only + be honored by components that enable + the WindowsHostProcessContainers + feature flag. Setting this field + without the feature flag will result + in errors when validating the Pod. + All of a Pod's containers must have + the same effective HostProcess value + (it is not allowed to have a mix + of HostProcess containers and non-HostProcess + containers). In addition, if HostProcess + is true then HostNetwork must also + be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows + to run the entrypoint of the container + process. Defaults to the user specified + in image metadata if unspecified. + May also be set in PodSecurityContext. + If set in both SecurityContext and + PodSecurityContext, the value specified + in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: 'StartupProbe indicates that + the Pod has successfully initialized. If + specified, no other probes are executed + until this completes successfully. If this + probe fails, the Pod will be restarted, + just as if the livenessProbe failed. This + can be used to provide different probe parameters + at the beginning of a Pod''s lifecycle, + when it might take a long time to load data + or warm a cache, than during steady-state + operation. This cannot be updated. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should + allocate a buffer for stdin in the container + runtime. If this is not set, reads from + stdin in the container will always result + in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime + should close the stdin channel after it + has been opened by a single attach. When + stdin is true the stdin stream will remain + open across multiple attach sessions. If + stdinOnce is set to true, stdin is opened + on container start, is empty until the first + client attaches to stdin, and then remains + open and accepts data until the client disconnects, + at which time stdin is closed and remains + closed until the container is restarted. + If this flag is false, a container processes + that reads from stdin will never receive + an EOF. Default is false + type: boolean + terminationMessagePath: + description: 'Optional: Path at which the + file to which the container''s termination + message will be written is mounted into + the container''s filesystem. Message written + is intended to be brief final status, such + as an assertion failure message. Will be + truncated by the node if greater than 4096 + bytes. The total message length across all + containers will be limited to 12kb. Defaults + to /dev/termination-log. Cannot be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination + message should be populated. File will use + the contents of terminationMessagePath to + populate the container status message on + both success and failure. FallbackToLogsOnError + will use the last chunk of container log + output if the termination message file is + empty and the container exited with an error. + The log output is limited to 2048 bytes + or 80 lines, whichever is smaller. Defaults + to File. Cannot be updated. + type: string + tty: + description: Whether this container should + allocate a TTY for itself, also requires + 'stdin' to be true. Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of + block devices to be used by the container. + items: + description: volumeDevice describes a mapping + of a raw block device within a container. + properties: + devicePath: + description: devicePath is the path + inside of the container that the device + will be mapped to. + type: string + name: + description: name must match the name + of a persistentVolumeClaim in the + pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the + container's filesystem. Cannot be updated. + items: + description: VolumeMount describes a mounting + of a Volume within a container. + properties: + mountPath: + description: Path within the container + at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: mountPropagation determines + how mounts are propagated from the + host to container and the other way + around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name + of a Volume. + type: string + readOnly: + description: Mounted read-only if true, + read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: Path within the volume + from which the container's volume + should be mounted. Defaults to "" + (volume's root). + type: string + subPathExpr: + description: Expanded path within the + volume from which the container's + volume should be mounted. Behaves + similarly to SubPath but environment + variable references $(VAR_NAME) are + expanded using the container's environment. + Defaults to "" (volume's root). SubPathExpr + and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. + If not specified, the container runtime's + default will be used, which might be configured + in the container image. Cannot be updated. + type: string + required: + - name + type: object + type: array + dnsConfig: + description: Specifies the DNS parameters of a pod. + Parameters specified here will be merged to the + generated DNS configuration based on DNSPolicy. + properties: + nameservers: + description: A list of DNS name server IP addresses. + This will be appended to the base nameservers + generated from DNSPolicy. Duplicated nameservers + will be removed. + items: + type: string + type: array + options: + description: A list of DNS resolver options. + This will be merged with the base options + generated from DNSPolicy. Duplicated entries + will be removed. Resolution options given + in Options will override those that appear + in the base DNSPolicy. + items: + description: PodDNSConfigOption defines DNS + resolver options of a pod. + properties: + name: + description: Required. + type: string + value: + type: string + type: object + type: array + searches: + description: A list of DNS search domains for + host-name lookup. This will be appended to + the base search paths generated from DNSPolicy. + Duplicated search paths will be removed. + items: + type: string + type: array + type: object + dnsPolicy: + description: Set DNS policy for the pod. Defaults + to "ClusterFirst". Valid values are 'ClusterFirstWithHostNet', + 'ClusterFirst', 'Default' or 'None'. DNS parameters + given in DNSConfig will be merged with the policy + selected with DNSPolicy. To have DNS options set + along with hostNetwork, you have to specify DNS + policy explicitly to 'ClusterFirstWithHostNet'. + type: string + enableServiceLinks: + description: 'EnableServiceLinks indicates whether + information about services should be injected + into pod''s environment variables, matching the + syntax of Docker links. Optional: Defaults to + true.' + type: boolean + ephemeralContainers: + description: List of ephemeral containers run in + this pod. Ephemeral containers may be run in an + existing pod to perform user-initiated actions + such as debugging. This list cannot be specified + when creating a pod, and it cannot be modified + by updating the pod spec. In order to add an ephemeral + container to an existing pod, use the pod's ephemeralcontainers + subresource. This field is alpha-level and is + only honored by servers that enable the EphemeralContainers + feature. + items: + description: An EphemeralContainer is a container + that may be added temporarily to an existing + pod for user-initiated activities such as debugging. + Ephemeral containers have no resource or scheduling + guarantees, and they will not be restarted when + they exit or when a pod is removed or restarted. + If an ephemeral container causes a pod to exceed + its resource allocation, the pod may be evicted. + Ephemeral containers may not be added by directly + updating the pod spec. They must be added via + the pod's ephemeralcontainers subresource, and + they will appear in the pod spec once added. + This is an alpha feature enabled by the EphemeralContainers + feature flag. + properties: + args: + description: 'Arguments to the entrypoint. + The docker image''s CMD is used if this + is not provided. Variable references $(VAR_NAME) + are expanded using the container''s environment. + If a variable cannot be resolved, the reference + in the input string will be unchanged. Double + $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal + "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable + exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed + within a shell. The docker image''s ENTRYPOINT + is used if this is not provided. Variable + references $(VAR_NAME) are expanded using + the container''s environment. If a variable + cannot be resolved, the reference in the + input string will be unchanged. Double $$ + are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal + "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable + exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables + to set in the container. Cannot be updated. + items: + description: EnvVar represents an environment + variable present in a Container. + properties: + name: + description: Name of the environment + variable. Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously + defined environment variables in the + container and any service environment + variables. If a variable cannot be + resolved, the reference in the input + string will be unchanged. Double $$ + are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: + i.e. "$$(VAR_NAME)" will produce the + string literal "$(VAR_NAME)". Escaped + references will never be expanded, + regardless of whether the variable + exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment + variable's value. Cannot be used if + value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a + ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether + the ConfigMap or its key must + be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of + the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, + `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource + of the container: only resources + limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory + and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a + secret in the pod's namespace + properties: + key: + description: The key of the + secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether + the Secret or its key must + be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined + within a source must be a C_IDENTIFIER. + All invalid keys will be reported as an + event when the container is starting. When + a key exists in multiple sources, the value + associated with the last source will take + precedence. Values defined by an Env with + a duplicate key will take precedence. Cannot + be updated. + items: + description: EnvFromSource represents the + source of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select + from + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the + ConfigMap must be defined + type: boolean + type: object + prefix: + description: An optional identifier + to prepend to each key in the ConfigMap. + Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the + Secret must be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Docker image name. More info: + https://kubernetes.io/docs/concepts/containers/images' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, + Never, IfNotPresent. Defaults to Always + if :latest tag is specified, or IfNotPresent + otherwise. Cannot be updated. More info: + https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Lifecycle is not allowed for + ephemeral containers. + properties: + postStart: + description: 'PostStart is called immediately + after a container is created. If the + handler fails, the container is terminated + and restarted according to its restart + policy. Other management of the container + blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: One and only one of the + following should be specified. Exec + specifies the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the + command is root ('/') in the + container's filesystem. The + command is simply exec'd, it + is not run inside a shell, so + traditional shell instructions + ('|', etc) won't work. To use + a shell, you need to explicitly + call out to that shell. Exit + status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the + http request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. + You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to + set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes + a custom header to be used + in HTTP probes + properties: + name: + description: The header + field name + type: string + value: + description: The header + field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on + the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for + connecting to the host. Defaults + to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: 'TCPSocket specifies + an action involving a TCP port. + TCP hooks not yet supported TODO: + implement a realistic TCP lifecycle + hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the + pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately + before a container is terminated due + to an API request or management event + such as liveness/startup probe failure, + preemption, resource contention, etc. + The handler is not called if the container + crashes or exits. The reason for termination + is passed to the handler. The Pod''s + termination grace period countdown begins + before the PreStop hooked is executed. + Regardless of the outcome of the handler, + the container will eventually terminate + within the Pod''s termination grace + period. Other management of the container + blocks until the hook completes or until + the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: One and only one of the + following should be specified. Exec + specifies the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the + command is root ('/') in the + container's filesystem. The + command is simply exec'd, it + is not run inside a shell, so + traditional shell instructions + ('|', etc) won't work. To use + a shell, you need to explicitly + call out to that shell. Exit + status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the + http request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. + You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to + set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes + a custom header to be used + in HTTP probes + properties: + name: + description: The header + field name + type: string + value: + description: The header + field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on + the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for + connecting to the host. Defaults + to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: 'TCPSocket specifies + an action involving a TCP port. + TCP hooks not yet supported TODO: + implement a realistic TCP lifecycle + hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the + pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: Probes are not allowed for ephemeral + containers. + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the ephemeral container + specified as a DNS_LABEL. This name must + be unique among all containers, init containers + and ephemeral containers. + type: string + ports: + description: Ports are not allowed for ephemeral + containers. + items: + description: ContainerPort represents a + network port in a single container. + properties: + containerPort: + description: Number of port to expose + on the pod's IP address. This must + be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the + external port to. + type: string + hostPort: + description: Number of port to expose + on the host. If specified, this must + be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this + must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must + be an IANA_SVC_NAME and unique within + the pod. Each named port in a pod + must have a unique name. Name for + the port that can be referred to by + services. + type: string + protocol: + default: TCP + description: Protocol for port. Must + be UDP, TCP, or SCTP. Defaults to + "TCP". + type: string + required: + - containerPort + type: object + type: array + readinessProbe: + description: Probes are not allowed for ephemeral + containers. + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: Resources are not allowed for + ephemeral containers. Ephemeral containers + use spare resources already allocated to + the pod. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum + amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum + amount of compute resources required. + If Requests is omitted for a container, + it defaults to Limits if that is explicitly + specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'Optional: SecurityContext defines + the security options the ephemeral container + should be run with. If set, the fields of + SecurityContext override the equivalent + fields of PodSecurityContext.' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation + controls whether a process can gain + more privileges than its parent process. + This bool directly controls if the no_new_privs + flag will be set on the container process. + AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged + 2) has CAP_SYS_ADMIN' + type: boolean + capabilities: + description: The capabilities to add/drop + when running containers. Defaults to + the default set of capabilities granted + by the container runtime. + properties: + add: + description: Added capabilities + items: + description: Capability represent + POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent + POSIX capabilities type + type: string + type: array + type: object + privileged: + description: Run container in privileged + mode. Processes in privileged containers + are essentially equivalent to root on + the host. Defaults to false. + type: boolean + procMount: + description: procMount denotes the type + of proc mount to use for the containers. + The default is DefaultProcMount which + uses the container runtime defaults + for readonly paths and masked paths. + This requires the ProcMountType feature + flag to be enabled. + type: string + readOnlyRootFilesystem: + description: Whether this container has + a read-only root filesystem. Default + is false. + type: boolean + runAsGroup: + description: The GID to run the entrypoint + of the container process. Uses runtime + default if unset. May also be set in + PodSecurityContext. If set in both + SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container + must run as a non-root user. If true, + the Kubelet will validate the image + at runtime to ensure that it does not + run as UID 0 (root) and fail to start + the container if it does. If unset or + false, no such validation will be performed. + May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint + of the container process. Defaults to + user specified in image metadata if + unspecified. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be + applied to the container. If unspecified, + the container runtime will allocate + a random SELinux context for each container. May + also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + properties: + level: + description: Level is SELinux level + label that applies to the container. + type: string + role: + description: Role is a SELinux role + label that applies to the container. + type: string + type: + description: Type is a SELinux type + label that applies to the container. + type: string + user: + description: User is a SELinux user + label that applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use + by this container. If seccomp options + are provided at both the pod & container + level, the container options override + the pod options. + properties: + localhostProfile: + description: localhostProfile indicates + a profile defined in a file on the + node should be used. The profile + must be preconfigured on the node + to work. Must be a descending path, + relative to the kubelet's configured + seccomp profile location. Must only + be set if type is "Localhost". + type: string + type: + description: "type indicates which + kind of seccomp profile will be + applied. Valid options are: \n Localhost + - a profile defined in a file on + the node should be used. RuntimeDefault + - the container runtime default + profile should be used. Unconfined + - no profile should be applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings + applied to all containers. If unspecified, + the options from the PodSecurityContext + will be used. If set in both SecurityContext + and PodSecurityContext, the value specified + in SecurityContext takes precedence. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is + where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA + credential spec named by the GMSACredentialSpecName + field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName + is the name of the GMSA credential + spec to use. + type: string + hostProcess: + description: HostProcess determines + if a container should be run as + a 'Host Process' container. This + field is alpha-level and will only + be honored by components that enable + the WindowsHostProcessContainers + feature flag. Setting this field + without the feature flag will result + in errors when validating the Pod. + All of a Pod's containers must have + the same effective HostProcess value + (it is not allowed to have a mix + of HostProcess containers and non-HostProcess + containers). In addition, if HostProcess + is true then HostNetwork must also + be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows + to run the entrypoint of the container + process. Defaults to the user specified + in image metadata if unspecified. + May also be set in PodSecurityContext. + If set in both SecurityContext and + PodSecurityContext, the value specified + in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: Probes are not allowed for ephemeral + containers. + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should + allocate a buffer for stdin in the container + runtime. If this is not set, reads from + stdin in the container will always result + in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime + should close the stdin channel after it + has been opened by a single attach. When + stdin is true the stdin stream will remain + open across multiple attach sessions. If + stdinOnce is set to true, stdin is opened + on container start, is empty until the first + client attaches to stdin, and then remains + open and accepts data until the client disconnects, + at which time stdin is closed and remains + closed until the container is restarted. + If this flag is false, a container processes + that reads from stdin will never receive + an EOF. Default is false + type: boolean + targetContainerName: + description: If set, the name of the container + from PodSpec that this ephemeral container + targets. The ephemeral container will be + run in the namespaces (IPC, PID, etc) of + this container. If not set then the ephemeral + container is run in whatever namespaces + are shared for the pod. Note that the container + runtime must support this feature. + type: string + terminationMessagePath: + description: 'Optional: Path at which the + file to which the container''s termination + message will be written is mounted into + the container''s filesystem. Message written + is intended to be brief final status, such + as an assertion failure message. Will be + truncated by the node if greater than 4096 + bytes. The total message length across all + containers will be limited to 12kb. Defaults + to /dev/termination-log. Cannot be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination + message should be populated. File will use + the contents of terminationMessagePath to + populate the container status message on + both success and failure. FallbackToLogsOnError + will use the last chunk of container log + output if the termination message file is + empty and the container exited with an error. + The log output is limited to 2048 bytes + or 80 lines, whichever is smaller. Defaults + to File. Cannot be updated. + type: string + tty: + description: Whether this container should + allocate a TTY for itself, also requires + 'stdin' to be true. Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of + block devices to be used by the container. + items: + description: volumeDevice describes a mapping + of a raw block device within a container. + properties: + devicePath: + description: devicePath is the path + inside of the container that the device + will be mapped to. + type: string + name: + description: name must match the name + of a persistentVolumeClaim in the + pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the + container's filesystem. Cannot be updated. + items: + description: VolumeMount describes a mounting + of a Volume within a container. + properties: + mountPath: + description: Path within the container + at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: mountPropagation determines + how mounts are propagated from the + host to container and the other way + around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name + of a Volume. + type: string + readOnly: + description: Mounted read-only if true, + read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: Path within the volume + from which the container's volume + should be mounted. Defaults to "" + (volume's root). + type: string + subPathExpr: + description: Expanded path within the + volume from which the container's + volume should be mounted. Behaves + similarly to SubPath but environment + variable references $(VAR_NAME) are + expanded using the container's environment. + Defaults to "" (volume's root). SubPathExpr + and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. + If not specified, the container runtime's + default will be used, which might be configured + in the container image. Cannot be updated. + type: string + required: + - name + type: object + type: array + hostAliases: + description: HostAliases is an optional list of + hosts and IPs that will be injected into the pod's + hosts file if specified. This is only valid for + non-hostNetwork pods. + items: + description: HostAlias holds the mapping between + IP and hostnames that will be injected as an + entry in the pod's hosts file. + properties: + hostnames: + description: Hostnames for the above IP address. + items: + type: string + type: array + ip: + description: IP address of the host file entry. + type: string + type: object + type: array + hostIPC: + description: 'Use the host''s ipc namespace. Optional: + Default to false.' + type: boolean + hostNetwork: + description: Host networking requested for this + pod. Use the host's network namespace. If this + option is set, the ports that will be used must + be specified. Default to false. + type: boolean + hostPID: + description: 'Use the host''s pid namespace. Optional: + Default to false.' + type: boolean + hostname: + description: Specifies the hostname of the Pod If + not specified, the pod's hostname will be set + to a system-defined value. + type: string + imagePullSecrets: + description: 'ImagePullSecrets is an optional list + of references to secrets in the same namespace + to use for pulling any of the images used by this + PodSpec. If specified, these secrets will be passed + to individual puller implementations for them + to use. For example, in the case of docker, only + DockerConfig type secrets are honored. More info: + https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod' + items: + description: LocalObjectReference contains enough + information to let you locate the referenced + object inside the same namespace. + properties: + name: + description: 'Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + type: array + initContainers: + description: 'List of initialization containers + belonging to the pod. Init containers are executed + in order prior to containers being started. If + any init container fails, the pod is considered + to have failed and is handled according to its + restartPolicy. The name for an init container + or normal container must be unique among all containers. + Init containers may not have Lifecycle actions, + Readiness probes, Liveness probes, or Startup + probes. The resourceRequirements of an init container + are taken into account during scheduling by finding + the highest request/limit for each resource type, + and then using the max of of that value or the + sum of the normal containers. Limits are applied + to init containers in a similar fashion. Init + containers cannot currently be added or removed. + Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/' + items: + description: A single application container that + you want to run within a pod. + properties: + args: + description: 'Arguments to the entrypoint. + The docker image''s CMD is used if this + is not provided. Variable references $(VAR_NAME) + are expanded using the container''s environment. + If a variable cannot be resolved, the reference + in the input string will be unchanged. Double + $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal + "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable + exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed + within a shell. The docker image''s ENTRYPOINT + is used if this is not provided. Variable + references $(VAR_NAME) are expanded using + the container''s environment. If a variable + cannot be resolved, the reference in the + input string will be unchanged. Double $$ + are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal + "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable + exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables + to set in the container. Cannot be updated. + items: + description: EnvVar represents an environment + variable present in a Container. + properties: + name: + description: Name of the environment + variable. Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously + defined environment variables in the + container and any service environment + variables. If a variable cannot be + resolved, the reference in the input + string will be unchanged. Double $$ + are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: + i.e. "$$(VAR_NAME)" will produce the + string literal "$(VAR_NAME)". Escaped + references will never be expanded, + regardless of whether the variable + exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment + variable's value. Cannot be used if + value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a + ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether + the ConfigMap or its key must + be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of + the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, + `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource + of the container: only resources + limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory + and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a + secret in the pod's namespace + properties: + key: + description: The key of the + secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether + the Secret or its key must + be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined + within a source must be a C_IDENTIFIER. + All invalid keys will be reported as an + event when the container is starting. When + a key exists in multiple sources, the value + associated with the last source will take + precedence. Values defined by an Env with + a duplicate key will take precedence. Cannot + be updated. + items: + description: EnvFromSource represents the + source of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select + from + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the + ConfigMap must be defined + type: boolean + type: object + prefix: + description: An optional identifier + to prepend to each key in the ConfigMap. + Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the + Secret must be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Docker image name. More info: + https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level + config management to default or override + container images in workload controllers + like Deployments and StatefulSets.' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, + Never, IfNotPresent. Defaults to Always + if :latest tag is specified, or IfNotPresent + otherwise. Cannot be updated. More info: + https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Actions that the management system + should take in response to container lifecycle + events. Cannot be updated. + properties: + postStart: + description: 'PostStart is called immediately + after a container is created. If the + handler fails, the container is terminated + and restarted according to its restart + policy. Other management of the container + blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: One and only one of the + following should be specified. Exec + specifies the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the + command is root ('/') in the + container's filesystem. The + command is simply exec'd, it + is not run inside a shell, so + traditional shell instructions + ('|', etc) won't work. To use + a shell, you need to explicitly + call out to that shell. Exit + status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the + http request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. + You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to + set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes + a custom header to be used + in HTTP probes + properties: + name: + description: The header + field name + type: string + value: + description: The header + field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on + the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for + connecting to the host. Defaults + to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: 'TCPSocket specifies + an action involving a TCP port. + TCP hooks not yet supported TODO: + implement a realistic TCP lifecycle + hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the + pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately + before a container is terminated due + to an API request or management event + such as liveness/startup probe failure, + preemption, resource contention, etc. + The handler is not called if the container + crashes or exits. The reason for termination + is passed to the handler. The Pod''s + termination grace period countdown begins + before the PreStop hooked is executed. + Regardless of the outcome of the handler, + the container will eventually terminate + within the Pod''s termination grace + period. Other management of the container + blocks until the hook completes or until + the termination grace period is reached. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: One and only one of the + following should be specified. Exec + specifies the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the + command is root ('/') in the + container's filesystem. The + command is simply exec'd, it + is not run inside a shell, so + traditional shell instructions + ('|', etc) won't work. To use + a shell, you need to explicitly + call out to that shell. Exit + status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the + http request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. + You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to + set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes + a custom header to be used + in HTTP probes + properties: + name: + description: The header + field name + type: string + value: + description: The header + field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on + the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for + connecting to the host. Defaults + to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: 'TCPSocket specifies + an action involving a TCP port. + TCP hooks not yet supported TODO: + implement a realistic TCP lifecycle + hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the + pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of + the port to access on the container. + Number must be in the range + 1 to 65535. Name must be an + IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: 'Periodic probe of container + liveness. Container will be restarted if + the probe fails. Cannot be updated. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the container specified + as a DNS_LABEL. Each container in a pod + must have a unique name (DNS_LABEL). Cannot + be updated. + type: string + ports: + description: List of ports to expose from + the container. Exposing a port here gives + the system additional information about + the network connections a container uses, + but is primarily informational. Not specifying + a port here DOES NOT prevent that port from + being exposed. Any port which is listening + on the default "0.0.0.0" address inside + a container will be accessible from the + network. Cannot be updated. + items: + description: ContainerPort represents a + network port in a single container. + properties: + containerPort: + description: Number of port to expose + on the pod's IP address. This must + be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the + external port to. + type: string + hostPort: + description: Number of port to expose + on the host. If specified, this must + be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this + must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must + be an IANA_SVC_NAME and unique within + the pod. Each named port in a pod + must have a unique name. Name for + the port that can be referred to by + services. + type: string + protocol: + default: TCP + description: Protocol for port. Must + be UDP, TCP, or SCTP. Defaults to + "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: 'Periodic probe of container + service readiness. Container will be removed + from service endpoints if the probe fails. + Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: 'Compute Resources required by + this container. Cannot be updated. More + info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum + amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum + amount of compute resources required. + If Requests is omitted for a container, + it defaults to Limits if that is explicitly + specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'SecurityContext defines the + security options the container should be + run with. If set, the fields of SecurityContext + override the equivalent fields of PodSecurityContext. + More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation + controls whether a process can gain + more privileges than its parent process. + This bool directly controls if the no_new_privs + flag will be set on the container process. + AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged + 2) has CAP_SYS_ADMIN' + type: boolean + capabilities: + description: The capabilities to add/drop + when running containers. Defaults to + the default set of capabilities granted + by the container runtime. + properties: + add: + description: Added capabilities + items: + description: Capability represent + POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent + POSIX capabilities type + type: string + type: array + type: object + privileged: + description: Run container in privileged + mode. Processes in privileged containers + are essentially equivalent to root on + the host. Defaults to false. + type: boolean + procMount: + description: procMount denotes the type + of proc mount to use for the containers. + The default is DefaultProcMount which + uses the container runtime defaults + for readonly paths and masked paths. + This requires the ProcMountType feature + flag to be enabled. + type: string + readOnlyRootFilesystem: + description: Whether this container has + a read-only root filesystem. Default + is false. + type: boolean + runAsGroup: + description: The GID to run the entrypoint + of the container process. Uses runtime + default if unset. May also be set in + PodSecurityContext. If set in both + SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container + must run as a non-root user. If true, + the Kubelet will validate the image + at runtime to ensure that it does not + run as UID 0 (root) and fail to start + the container if it does. If unset or + false, no such validation will be performed. + May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint + of the container process. Defaults to + user specified in image metadata if + unspecified. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be + applied to the container. If unspecified, + the container runtime will allocate + a random SELinux context for each container. May + also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + properties: + level: + description: Level is SELinux level + label that applies to the container. + type: string + role: + description: Role is a SELinux role + label that applies to the container. + type: string + type: + description: Type is a SELinux type + label that applies to the container. + type: string + user: + description: User is a SELinux user + label that applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use + by this container. If seccomp options + are provided at both the pod & container + level, the container options override + the pod options. + properties: + localhostProfile: + description: localhostProfile indicates + a profile defined in a file on the + node should be used. The profile + must be preconfigured on the node + to work. Must be a descending path, + relative to the kubelet's configured + seccomp profile location. Must only + be set if type is "Localhost". + type: string + type: + description: "type indicates which + kind of seccomp profile will be + applied. Valid options are: \n Localhost + - a profile defined in a file on + the node should be used. RuntimeDefault + - the container runtime default + profile should be used. Unconfined + - no profile should be applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings + applied to all containers. If unspecified, + the options from the PodSecurityContext + will be used. If set in both SecurityContext + and PodSecurityContext, the value specified + in SecurityContext takes precedence. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is + where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA + credential spec named by the GMSACredentialSpecName + field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName + is the name of the GMSA credential + spec to use. + type: string + hostProcess: + description: HostProcess determines + if a container should be run as + a 'Host Process' container. This + field is alpha-level and will only + be honored by components that enable + the WindowsHostProcessContainers + feature flag. Setting this field + without the feature flag will result + in errors when validating the Pod. + All of a Pod's containers must have + the same effective HostProcess value + (it is not allowed to have a mix + of HostProcess containers and non-HostProcess + containers). In addition, if HostProcess + is true then HostNetwork must also + be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows + to run the entrypoint of the container + process. Defaults to the user specified + in image metadata if unspecified. + May also be set in PodSecurityContext. + If set in both SecurityContext and + PodSecurityContext, the value specified + in SecurityContext takes precedence. + type: string + type: object + type: object + startupProbe: + description: 'StartupProbe indicates that + the Pod has successfully initialized. If + specified, no other probes are executed + until this completes successfully. If this + probe fails, the Pod will be restarted, + just as if the livenessProbe failed. This + can be used to provide different probe parameters + at the beginning of a Pod''s lifecycle, + when it might take a long time to load data + or warm a cache, than during steady-state + operation. This cannot be updated. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: One and only one of the following + should be specified. Exec specifies + the action to take. + properties: + command: + description: Command is the command + line to execute inside the container, + the working directory for the command is + root ('/') in the container's filesystem. + The command is simply exec'd, it + is not run inside a shell, so traditional + shell instructions ('|', etc) won't + work. To use a shell, you need to + explicitly call out to that shell. + Exit status of 0 is treated as live/healthy + and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures + for the probe to be considered failed + after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + type: integer + httpGet: + description: HTTPGet specifies the http + request to perform. + properties: + host: + description: Host name to connect + to, defaults to the pod IP. You + probably want to set "Host" in httpHeaders + instead. + type: string + httpHeaders: + description: Custom headers to set + in the request. HTTP allows repeated + headers. + items: + description: HTTPHeader describes + a custom header to be used in + HTTP probes + properties: + name: + description: The header field + name + type: string + value: + description: The header field + value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the + HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after + the container has started before liveness + probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to + perform the probe. Default to 10 seconds. + Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes + for the probe to be considered successful + after having failed. Defaults to 1. + Must be 1 for liveness and startup. + Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: 'TCPSocket specifies an action + involving a TCP port. TCP hooks not + yet supported TODO: implement a realistic + TCP lifecycle hook' + properties: + host: + description: 'Optional: Host name + to connect to, defaults to the pod + IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the + port to access on the container. + Number must be in the range 1 to + 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds + the pod needs to terminate gracefully + upon probe failure. The grace period + is the duration in seconds after the + processes running in the pod are sent + a termination signal and the time when + the processes are forcibly halted with + a kill signal. Set this value longer + than the expected cleanup time for your + process. If this value is nil, the pod's + terminationGracePeriodSeconds will be + used. Otherwise, this value overrides + the value provided by the pod spec. + Value must be non-negative integer. + The value zero indicates stop immediately + via the kill signal (no opportunity + to shut down). This is a beta field + and requires enabling ProbeTerminationGracePeriod + feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after + which the probe times out. Defaults + to 1 second. Minimum value is 1. More + info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should + allocate a buffer for stdin in the container + runtime. If this is not set, reads from + stdin in the container will always result + in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime + should close the stdin channel after it + has been opened by a single attach. When + stdin is true the stdin stream will remain + open across multiple attach sessions. If + stdinOnce is set to true, stdin is opened + on container start, is empty until the first + client attaches to stdin, and then remains + open and accepts data until the client disconnects, + at which time stdin is closed and remains + closed until the container is restarted. + If this flag is false, a container processes + that reads from stdin will never receive + an EOF. Default is false + type: boolean + terminationMessagePath: + description: 'Optional: Path at which the + file to which the container''s termination + message will be written is mounted into + the container''s filesystem. Message written + is intended to be brief final status, such + as an assertion failure message. Will be + truncated by the node if greater than 4096 + bytes. The total message length across all + containers will be limited to 12kb. Defaults + to /dev/termination-log. Cannot be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination + message should be populated. File will use + the contents of terminationMessagePath to + populate the container status message on + both success and failure. FallbackToLogsOnError + will use the last chunk of container log + output if the termination message file is + empty and the container exited with an error. + The log output is limited to 2048 bytes + or 80 lines, whichever is smaller. Defaults + to File. Cannot be updated. + type: string + tty: + description: Whether this container should + allocate a TTY for itself, also requires + 'stdin' to be true. Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of + block devices to be used by the container. + items: + description: volumeDevice describes a mapping + of a raw block device within a container. + properties: + devicePath: + description: devicePath is the path + inside of the container that the device + will be mapped to. + type: string + name: + description: name must match the name + of a persistentVolumeClaim in the + pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the + container's filesystem. Cannot be updated. + items: + description: VolumeMount describes a mounting + of a Volume within a container. + properties: + mountPath: + description: Path within the container + at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: mountPropagation determines + how mounts are propagated from the + host to container and the other way + around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name + of a Volume. + type: string + readOnly: + description: Mounted read-only if true, + read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: Path within the volume + from which the container's volume + should be mounted. Defaults to "" + (volume's root). + type: string + subPathExpr: + description: Expanded path within the + volume from which the container's + volume should be mounted. Behaves + similarly to SubPath but environment + variable references $(VAR_NAME) are + expanded using the container's environment. + Defaults to "" (volume's root). SubPathExpr + and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. + If not specified, the container runtime's + default will be used, which might be configured + in the container image. Cannot be updated. + type: string + required: + - name + type: object + type: array + nodeName: + description: NodeName is a request to schedule this + pod onto a specific node. If it is non-empty, + the scheduler simply schedules this pod onto that + node, assuming that it fits resource requirements. + type: string + nodeSelector: + additionalProperties: + type: string + description: 'NodeSelector is a selector which must + be true for the pod to fit on a node. Selector + which must match a node''s labels for the pod + to be scheduled on that node. More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' + type: object + x-kubernetes-map-type: atomic + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Overhead represents the resource overhead + associated with running a pod for a given RuntimeClass. + This field will be autopopulated at admission + time by the RuntimeClass admission controller. + If the RuntimeClass admission controller is enabled, + overhead must not be set in Pod create requests. + The RuntimeClass admission controller will reject + Pod create requests which have the overhead already + set. If RuntimeClass is configured and selected + in the PodSpec, Overhead will be set to the value + defined in the corresponding RuntimeClass, otherwise + it will remain unset and treated as zero. More + info: https://git.k8s.io/enhancements/keps/sig-node/688-pod-overhead/README.md + This field is beta-level as of Kubernetes v1.18, + and is only honored by servers that enable the + PodOverhead feature.' + type: object + preemptionPolicy: + description: PreemptionPolicy is the Policy for + preempting pods with lower priority. One of Never, + PreemptLowerPriority. Defaults to PreemptLowerPriority + if unset. This field is beta-level, gated by the + NonPreemptingPriority feature-gate. + type: string + priority: + description: The priority value. Various system + components use this field to find the priority + of the pod. When Priority Admission Controller + is enabled, it prevents users from setting this + field. The admission controller populates this + field from PriorityClassName. The higher the value, + the higher the priority. + format: int32 + type: integer + priorityClassName: + description: If specified, indicates the pod's priority. + "system-node-critical" and "system-cluster-critical" + are two special keywords which indicate the highest + priorities with the former being the highest priority. + Any other name must be defined by creating a PriorityClass + object with that name. If not specified, the pod + priority will be default or zero if there is no + default. + type: string + readinessGates: + description: 'If specified, all readiness gates + will be evaluated for pod readiness. A pod is + ready when all its containers are ready AND all + conditions specified in the readiness gates have + status equal to "True" More info: https://git.k8s.io/enhancements/keps/sig-network/580-pod-readiness-gates' + items: + description: PodReadinessGate contains the reference + to a pod condition + properties: + conditionType: + description: ConditionType refers to a condition + in the pod's condition list with matching + type. + type: string + required: + - conditionType + type: object + type: array + restartPolicy: + description: 'Restart policy for all containers + within the pod. One of Always, OnFailure, Never. + Default to Always. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy' + type: string + runtimeClassName: + description: 'RuntimeClassName refers to a RuntimeClass + object in the node.k8s.io group, which should + be used to run this pod. If no RuntimeClass resource + matches the named class, the pod will not be run. + If unset or empty, the "legacy" RuntimeClass will + be used, which is an implicit class with an empty + definition that uses the default runtime handler. + More info: https://git.k8s.io/enhancements/keps/sig-node/585-runtime-class + This is a beta feature as of Kubernetes v1.14.' + type: string + schedulerName: + description: If specified, the pod will be dispatched + by specified scheduler. If not specified, the + pod will be dispatched by default scheduler. + type: string + securityContext: + description: 'SecurityContext holds pod-level security + attributes and common container settings. Optional: + Defaults to empty. See type description for default + values of each field.' + properties: + fsGroup: + description: "A special supplemental group that + applies to all containers in a pod. Some volume + types allow the Kubelet to change the ownership + of that volume to be owned by the pod: \n + 1. The owning GID will be the FSGroup 2. The + setgid bit is set (new files created in the + volume will be owned by FSGroup) 3. The permission + bits are OR'd with rw-rw---- \n If unset, + the Kubelet will not modify the ownership + and permissions of any volume." + format: int64 + type: integer + fsGroupChangePolicy: + description: 'fsGroupChangePolicy defines behavior + of changing ownership and permission of the + volume before being exposed inside Pod. This + field will only apply to volume types which + support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume + types such as: secret, configmaps and emptydir. + Valid values are "OnRootMismatch" and "Always". + If not specified, "Always" is used.' + type: string + runAsGroup: + description: The GID to run the entrypoint of + the container process. Uses runtime default + if unset. May also be set in SecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence for that container. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must + run as a non-root user. If true, the Kubelet + will validate the image at runtime to ensure + that it does not run as UID 0 (root) and fail + to start the container if it does. If unset + or false, no such validation will be performed. + May also be set in SecurityContext. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of + the container process. Defaults to user specified + in image metadata if unspecified. May also + be set in SecurityContext. If set in both + SecurityContext and PodSecurityContext, the + value specified in SecurityContext takes precedence + for that container. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied + to all containers. If unspecified, the container + runtime will allocate a random SELinux context + for each container. May also be set in SecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence for that container. + properties: + level: + description: Level is SELinux level label + that applies to the container. + type: string + role: + description: Role is a SELinux role label + that applies to the container. + type: string + type: + description: Type is a SELinux type label + that applies to the container. + type: string + user: + description: User is a SELinux user label + that applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by the + containers in this pod. + properties: + localhostProfile: + description: localhostProfile indicates + a profile defined in a file on the node + should be used. The profile must be preconfigured + on the node to work. Must be a descending + path, relative to the kubelet's configured + seccomp profile location. Must only be + set if type is "Localhost". + type: string + type: + description: "type indicates which kind + of seccomp profile will be applied. Valid + options are: \n Localhost - a profile + defined in a file on the node should be + used. RuntimeDefault - the container runtime + default profile should be used. Unconfined + - no profile should be applied." + type: string + required: + - type + type: object + supplementalGroups: + description: A list of groups applied to the + first process run in each container, in addition + to the container's primary GID. If unspecified, + no groups will be added to any container. + items: + format: int64 + type: integer + type: array + sysctls: + description: Sysctls hold a list of namespaced + sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail + to launch. + items: + description: Sysctl defines a kernel parameter + to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: The Windows specific settings applied + to all containers. If unspecified, the options + within a container's SecurityContext will + be used. If set in both SecurityContext and + PodSecurityContext, the value specified in + SecurityContext takes precedence. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where + the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential + spec named by the GMSACredentialSpecName + field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the + name of the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a + container should be run as a 'Host Process' + container. This field is alpha-level and + will only be honored by components that + enable the WindowsHostProcessContainers + feature flag. Setting this field without + the feature flag will result in errors + when validating the Pod. All of a Pod's + containers must have the same effective + HostProcess value (it is not allowed to + have a mix of HostProcess containers and + non-HostProcess containers). In addition, + if HostProcess is true then HostNetwork + must also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to + run the entrypoint of the container process. + Defaults to the user specified in image + metadata if unspecified. May also be set + in PodSecurityContext. If set in both + SecurityContext and PodSecurityContext, + the value specified in SecurityContext + takes precedence. + type: string + type: object + type: object + serviceAccount: + description: 'DeprecatedServiceAccount is a depreciated + alias for ServiceAccountName. Deprecated: Use + serviceAccountName instead.' + type: string + serviceAccountName: + description: 'ServiceAccountName is the name of + the ServiceAccount to use to run this pod. More + info: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/' + type: string + setHostnameAsFQDN: + description: If true the pod's hostname will be + configured as the pod's FQDN, rather than the + leaf name (the default). In Linux containers, + this means setting the FQDN in the hostname field + of the kernel (the nodename field of struct utsname). + In Windows containers, this means setting the + registry value of hostname for the registry key + HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters + to FQDN. If a pod does not have FQDN, this has + no effect. Default to false. + type: boolean + shareProcessNamespace: + description: 'Share a single process namespace between + all of the containers in a pod. When this is set + containers will be able to view and signal processes + from other containers in the same pod, and the + first process in each container will not be assigned + PID 1. HostPID and ShareProcessNamespace cannot + both be set. Optional: Default to false.' + type: boolean + subdomain: + description: If specified, the fully qualified Pod + hostname will be "...svc.". If not specified, + the pod will not have a domainname at all. + type: string + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully. May be decreased + in delete request. Value must be non-negative + integer. The value zero indicates stop immediately + via the kill signal (no opportunity to shut down). + If this value is nil, the default grace period + will be used instead. The grace period is the + duration in seconds after the processes running + in the pod are sent a termination signal and the + time when the processes are forcibly halted with + a kill signal. Set this value longer than the + expected cleanup time for your process. Defaults + to 30 seconds. + format: int64 + type: integer + tolerations: + description: If specified, the pod's tolerations. + items: + description: The pod this Toleration is attached + to tolerates any taint that matches the triple + using the matching operator + . + properties: + effect: + description: Effect indicates the taint effect + to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, + PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the + toleration applies to. Empty means match + all taint keys. If the key is empty, operator + must be Exists; this combination means to + match all values and all keys. + type: string + operator: + description: Operator represents a key's relationship + to the value. Valid operators are Exists + and Equal. Defaults to Equal. Exists is + equivalent to wildcard for value, so that + a pod can tolerate all taints of a particular + category. + type: string + tolerationSeconds: + description: TolerationSeconds represents + the period of time the toleration (which + must be of effect NoExecute, otherwise this + field is ignored) tolerates the taint. By + default, it is not set, which means tolerate + the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict + immediately) by the system. + format: int64 + type: integer + value: + description: Value is the taint value the + toleration matches to. If the operator is + Exists, the value should be empty, otherwise + just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + description: TopologySpreadConstraints describes + how a group of pods ought to spread across topology + domains. Scheduler will schedule pods in a way + which abides by the constraints. All topologySpreadConstraints + are ANDed. + items: + description: TopologySpreadConstraint specifies + how to spread matching pods among the given + topology. + properties: + labelSelector: + description: LabelSelector is used to find + matching pods. Pods that match this label + selector are counted to determine the number + of pods in their corresponding topology + domain. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. This + array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + maxSkew: + description: 'MaxSkew describes the degree + to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, + it is the maximum permitted difference between + the number of matching pods in the target + topology and the global minimum. For example, + in a 3-zone cluster, MaxSkew is set to 1, + and pods with the same labelSelector spread + as 1/1/0: | zone1 | zone2 | zone3 | | P | P | | + - if MaxSkew is 1, incoming pod can only + be scheduled to zone3 to become 1/1/1; scheduling + it onto zone1(zone2) would make the ActualSkew(2-0) + on zone1(zone2) violate MaxSkew(1). - if + MaxSkew is 2, incoming pod can be scheduled + onto any zone. When `whenUnsatisfiable=ScheduleAnyway`, + it is used to give higher precedence to + topologies that satisfy it. It''s a required + field. Default value is 1 and 0 is not allowed.' + format: int32 + type: integer + topologyKey: + description: TopologyKey is the key of node + labels. Nodes that have a label with this + key and identical values are considered + to be in the same topology. We consider + each as a "bucket", and try + to put balanced number of pods into each + bucket. It's a required field. + type: string + whenUnsatisfiable: + description: 'WhenUnsatisfiable indicates + how to deal with a pod if it doesn''t satisfy + the spread constraint. - DoNotSchedule (default) + tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to + schedule the pod in any location, but + giving higher precedence to topologies that + would help reduce the skew. A constraint + is considered "Unsatisfiable" for an incoming + pod if and only if every possible node assigment + for that pod would violate "MaxSkew" on + some topology. For example, in a 3-zone + cluster, MaxSkew is set to 1, and pods with + the same labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, + incoming pod can only be scheduled to zone2(zone3) + to become 3/2/1(3/1/2) as ActualSkew(2-1) + on zone2(zone3) satisfies MaxSkew(1). In + other words, the cluster can still be imbalanced, + but scheduler won''t make it *more* imbalanced. + It''s a required field.' + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + description: 'List of volumes that can be mounted + by containers belonging to the pod. More info: + https://kubernetes.io/docs/concepts/storage/volumes' + items: + description: Volume represents a named volume + in a pod that may be accessed by any container + in the pod. + properties: + awsElasticBlockStore: + description: 'AWSElasticBlockStore represents + an AWS Disk resource that is attached to + a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'Filesystem type of the volume + that you want to mount. Tip: Ensure + that the filesystem type is supported + by the host operating system. Examples: + "ext4", "xfs", "ntfs". Implicitly inferred + to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the + filesystem from compromising the machine' + type: string + partition: + description: 'The partition in the volume + that you want to mount. If omitted, + the default is to mount by volume name. + Examples: For volume /dev/sda1, you + specify the partition as "1". Similarly, + the volume partition for /dev/sda is + "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'Specify "true" to force + and set the ReadOnly property in VolumeMounts + to "true". If omitted, the default is + "false". More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'Unique ID of the persistent + disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: AzureDisk represents an Azure + Data Disk mount on the host and bind mount + to the pod. + properties: + cachingMode: + description: 'Host Caching mode: None, + Read Only, Read Write.' + type: string + diskName: + description: The Name of the data disk + in the blob storage + type: string + diskURI: + description: The URI the data disk in + the blob storage + type: string + fsType: + description: Filesystem type to mount. + Must be a filesystem type supported + by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to + be "ext4" if unspecified. + type: string + kind: + description: 'Expected values Shared: + multiple blob disks per storage account Dedicated: + single blob disk per storage account Managed: + azure managed data disk (only in managed + availability set). defaults to shared' + type: string + readOnly: + description: Defaults to false (read/write). + ReadOnly here will force the ReadOnly + setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: AzureFile represents an Azure + File Service mount on the host and bind + mount to the pod. + properties: + readOnly: + description: Defaults to false (read/write). + ReadOnly here will force the ReadOnly + setting in VolumeMounts. + type: boolean + secretName: + description: the name of secret that contains + Azure Storage Account Name and Key + type: string + shareName: + description: Share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: CephFS represents a Ceph FS mount + on the host that shares a pod's lifetime + properties: + monitors: + description: 'Required: Monitors is a + collection of Ceph monitors More info: + https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'Optional: Used as the mounted + root, rather than the full Ceph tree, + default is /' + type: string + readOnly: + description: 'Optional: Defaults to false + (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'Optional: SecretFile is + the path to key ring for User, default + is /etc/ceph/user.secret More info: + https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'Optional: SecretRef is reference + to the authentication secret for User, + default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + user: + description: 'Optional: User is the rados + user name, default is admin More info: + https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'Cinder represents a cinder volume + attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'Filesystem type to mount. + Must be a filesystem type supported + by the host operating system. Examples: + "ext4", "xfs", "ntfs". Implicitly inferred + to be "ext4" if unspecified. More info: + https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'Optional: Defaults to false + (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'Optional: points to a secret + object containing parameters used to + connect to OpenStack.' + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + volumeID: + description: 'volume id used to identify + the volume in cinder. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: ConfigMap represents a configMap + that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits used + to set permissions on created files + by default. Must be an octal value between + 0000 and 0777 or a decimal value between + 0 and 511. YAML accepts both octal and + decimal values, JSON requires decimal + values for mode bits. Defaults to 0644. + Directories within the path are not + affected by this setting. This might + be in conflict with other options that + affect the file mode, like fsGroup, + and the result can be other mode bits + set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value + pair in the Data field of the referenced + ConfigMap will be projected into the + volume as a file whose name is the key + and content is the value. If specified, + the listed keys will be projected into + the specified paths, and unlisted keys + will not be present. If a key is specified + which is not present in the ConfigMap, + the volume setup will error unless it + is marked optional. Paths must be relative + and may not contain the '..' path or + start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits + used to set permissions on this + file. Must be an octal value between + 0000 and 0777 or a decimal value + between 0 and 511. YAML accepts + both octal and decimal values, + JSON requires decimal values for + mode bits. If not specified, the + volume defaultMode will be used. + This might be in conflict with + other options that affect the + file mode, like fsGroup, and the + result can be other mode bits + set.' + format: int32 + type: integer + path: + description: The relative path of + the file to map the key to. May + not be an absolute path. May not + contain the path element '..'. + May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More + info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + csi: + description: CSI (Container Storage Interface) + represents ephemeral storage that is handled + by certain external CSI drivers (Beta feature). + properties: + driver: + description: Driver is the name of the + CSI driver that handles this volume. + Consult with your admin for the correct + name as registered in the cluster. + type: string + fsType: + description: Filesystem type to mount. + Ex. "ext4", "xfs", "ntfs". If not provided, + the empty value is passed to the associated + CSI driver which will determine the + default filesystem to apply. + type: string + nodePublishSecretRef: + description: NodePublishSecretRef is a + reference to the secret object containing + sensitive information to pass to the + CSI driver to complete the CSI NodePublishVolume + and NodeUnpublishVolume calls. This + field is optional, and may be empty + if no secret is required. If the secret + object contains more than one secret, + all secret references are passed. + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + readOnly: + description: Specifies a read-only configuration + for the volume. Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: VolumeAttributes stores driver-specific + properties that are passed to the CSI + driver. Consult your driver's documentation + for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: DownwardAPI represents downward + API about the pod that should populate this + volume + properties: + defaultMode: + description: 'Optional: mode bits to use + on created files by default. Must be + a Optional: mode bits used to set permissions + on created files by default. Must be + an octal value between 0000 and 0777 + or a decimal value between 0 and 511. + YAML accepts both octal and decimal + values, JSON requires decimal values + for mode bits. Defaults to 0644. Directories + within the path are not affected by + this setting. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can + be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward + API volume file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits + used to set permissions on this + file, must be an octal value between + 0000 and 0777 or a decimal value + between 0 and 511. YAML accepts + both octal and decimal values, + JSON requires decimal values for + mode bits. If not specified, the + volume defaultMode will be used. + This might be in conflict with + other options that affect the + file mode, like fsGroup, and the + result can be other mode bits + set.' + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource + of the container: only resources + limits and requests (limits.cpu, + limits.memory, requests.cpu and + requests.memory) are currently + supported.' + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'EmptyDir represents a temporary + directory that shares a pod''s lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'What type of storage medium + should back this directory. The default + is "" which means to use the node''s + default medium. Must be an empty string + (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: 'Total amount of local storage + required for this EmptyDir volume. The + size limit is also applicable for memory + medium. The maximum usage on memory + medium EmptyDir would be the minimum + value between the SizeLimit specified + here and the sum of memory limits of + all containers in a pod. The default + is nil which means that the limit is + undefined. More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: "Ephemeral represents a volume + that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod + that defines it - it will be created before + the pod starts, and deleted when the pod + is removed. \n Use this if: a) the volume + is only needed while the pod runs, b) features + of normal volumes like restoring from snapshot + or capacity tracking are needed, c) the + storage driver is specified through a storage + class, and d) the storage driver supports + dynamic volume provisioning through a + PersistentVolumeClaim (see EphemeralVolumeSource + for more information on the connection + between this volume type and PersistentVolumeClaim). + \n Use PersistentVolumeClaim or one of the + vendor-specific APIs for volumes that persist + for longer than the lifecycle of an individual + pod. \n Use CSI for light-weight local ephemeral + volumes if the CSI driver is meant to be + used that way - see the documentation of + the driver for more information." + properties: + volumeClaimTemplate: + description: "Will be used to create a + stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource + is embedded will be the owner of the + PVC, i.e. the PVC will be deleted together + with the pod. The name of the PVC will + be `-` where + `` is the name from the + `PodSpec.Volumes` array entry. Pod validation + will reject the pod if the concatenated + name is not valid for a PVC (for example, + too long). \n An existing PVC with that + name that is not owned by the pod will + *not* be used for the pod to avoid using + an unrelated volume by mistake. Starting + the pod is then blocked until the unrelated + PVC is removed. If such a pre-created + PVC is meant to be used by the pod, + the PVC has to updated with an owner + reference to the pod once the pod exists. + Normally this should not be necessary, + but it may be useful when manually reconstructing + a broken cluster. \n This field is read-only + and no changes will be made by Kubernetes + to the PVC after it has been created." + properties: + metadata: + description: May contain labels and + annotations that will be copied + into the PVC when creating it. No + other fields are allowed and will + be rejected during validation. + type: object + spec: + description: The specification for + the PersistentVolumeClaim. The entire + content is copied unchanged into + the PVC that gets created from this + template. The same fields as in + a PersistentVolumeClaim are also + valid here. + properties: + accessModes: + description: 'AccessModes contains + the desired access modes the + volume should have. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + items: + type: string + type: array + dataSource: + description: 'This field can be + used to specify either: * An + existing VolumeSnapshot object + (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external + controller can support the specified + data source, it will create + a new volume based on the contents + of the specified data source. + If the AnyVolumeDataSource feature + gate is enabled, this field + will always have the same contents + as the DataSourceRef field.' + properties: + apiGroup: + description: APIGroup is the + group for the resource being + referenced. If APIGroup + is not specified, the specified + Kind must be in the core + API group. For any other + third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type + of resource being referenced + type: string + name: + description: Name is the name + of resource being referenced + type: string + required: + - kind + - name + type: object + dataSourceRef: + description: 'Specifies the object + from which to populate the volume + with data, if a non-empty volume + is desired. This may be any + local object from a non-empty + API group (non core object) + or a PersistentVolumeClaim object. + When this field is specified, + volume binding will only succeed + if the type of the specified + object matches some installed + volume populator or dynamic + provisioner. This field will + replace the functionality of + the DataSource field and as + such if both fields are non-empty, + they must have the same value. + For backwards compatibility, + both fields (DataSource and + DataSourceRef) will be set to + the same value automatically + if one of them is empty and + the other is non-empty. There + are two important differences + between DataSource and DataSourceRef: + * While DataSource only allows + two specific types of objects, + DataSourceRef allows any non-core + object, as well as PersistentVolumeClaim + objects.' + properties: + apiGroup: + description: APIGroup is the + group for the resource being + referenced. If APIGroup + is not specified, the specified + Kind must be in the core + API group. For any other + third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type + of resource being referenced + type: string + name: + description: Name is the name + of resource being referenced + type: string + required: + - kind + - name + type: object + resources: + description: 'Resources represents + the minimum resources the volume + should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes + the maximum amount of compute + resources allowed. More + info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes + the minimum amount of compute + resources required. If Requests + is omitted for a container, + it defaults to Limits if + that is explicitly specified, + otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + selector: + description: A label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions + is a list of label selector + requirements. The requirements + are ANDed. + items: + description: A label selector + requirement is a selector + that contains values, + a key, and an operator + that relates the key and + values. + properties: + key: + description: key is + the label key that + the selector applies + to. + type: string + operator: + description: operator + represents a key's + relationship to a + set of values. Valid + operators are In, + NotIn, Exists and + DoesNotExist. + type: string + values: + description: values + is an array of string + values. If the operator + is In or NotIn, the + values array must + be non-empty. If the + operator is Exists + or DoesNotExist, the + values array must + be empty. This array + is replaced during + a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is + a map of {key,value} pairs. + A single {key,value} in + the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", + the operator is "In", and + the values array contains + only "value". The requirements + are ANDed. + type: object + type: object + storageClassName: + description: 'Name of the StorageClass + required by the claim. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + type: string + volumeMode: + description: volumeMode defines + what type of volume is required + by the claim. Value of Filesystem + is implied when not included + in claim spec. + type: string + volumeName: + description: VolumeName is the + binding reference to the PersistentVolume + backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: FC represents a Fibre Channel + resource that is attached to a kubelet's + host machine and then exposed to the pod. + properties: + fsType: + description: 'Filesystem type to mount. + Must be a filesystem type supported + by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to + be "ext4" if unspecified. TODO: how + do we prevent errors in the filesystem + from compromising the machine' + type: string + lun: + description: 'Optional: FC target lun + number' + format: int32 + type: integer + readOnly: + description: 'Optional: Defaults to false + (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + wwids: + description: 'Optional: FC volume world + wide identifiers (wwids) Either wwids + or combination of targetWWNs and lun + must be set, but not both simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolume represents a generic + volume resource that is provisioned/attached + using an exec based plugin. + properties: + driver: + description: Driver is the name of the + driver to use for this volume. + type: string + fsType: + description: Filesystem type to mount. + Must be a filesystem type supported + by the host operating system. Ex. "ext4", + "xfs", "ntfs". The default filesystem + depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'Optional: Extra command + options if any.' + type: object + readOnly: + description: 'Optional: Defaults to false + (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'Optional: SecretRef is reference + to the secret object containing sensitive + information to pass to the plugin scripts. + This may be empty if no secret object + is specified. If the secret object contains + more than one secret, all secrets are + passed to the plugin scripts.' + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + required: + - driver + type: object + flocker: + description: Flocker represents a Flocker + volume attached to a kubelet's host machine. + This depends on the Flocker control service + being running + properties: + datasetName: + description: Name of the dataset stored + as metadata -> name on the dataset for + Flocker should be considered as deprecated + type: string + datasetUUID: + description: UUID of the dataset. This + is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'GCEPersistentDisk represents + a GCE Disk resource that is attached to + a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'Filesystem type of the volume + that you want to mount. Tip: Ensure + that the filesystem type is supported + by the host operating system. Examples: + "ext4", "xfs", "ntfs". Implicitly inferred + to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the + filesystem from compromising the machine' + type: string + partition: + description: 'The partition in the volume + that you want to mount. If omitted, + the default is to mount by volume name. + Examples: For volume /dev/sda1, you + specify the partition as "1". Similarly, + the volume partition for /dev/sda is + "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'Unique name of the PD resource + in GCE. Used to identify the disk in + GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'ReadOnly here will force + the ReadOnly setting in VolumeMounts. + Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'GitRepo represents a git repository + at a particular revision. DEPRECATED: GitRepo + is deprecated. To provision a container + with a git repo, mount an EmptyDir into + an InitContainer that clones the repo using + git, then mount the EmptyDir into the Pod''s + container.' + properties: + directory: + description: Target directory name. Must + not contain or start with '..'. If + '.' is supplied, the volume directory + will be the git repository. Otherwise, + if specified, the volume will contain + the git repository in the subdirectory + with the given name. + type: string + repository: + description: Repository URL + type: string + revision: + description: Commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'Glusterfs represents a Glusterfs + mount on the host that shares a pod''s lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'EndpointsName is the endpoint + name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'Path is the Glusterfs volume + path. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'ReadOnly here will force + the Glusterfs volume to be mounted with + read-only permissions. Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'HostPath represents a pre-existing + file or directory on the host machine that + is directly exposed to the container. This + is generally used for system agents or other + privileged things that are allowed to see + the host machine. Most containers will NOT + need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who + can use host directory mounts and who can/can + not mount host directories as read/write.' + properties: + path: + description: 'Path of the directory on + the host. If the path is a symlink, + it will follow the link to the real + path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'Type for HostPath Volume + Defaults to "" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'ISCSI represents an ISCSI Disk + resource that is attached to a kubelet''s + host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: whether support iSCSI Discovery + CHAP authentication + type: boolean + chapAuthSession: + description: whether support iSCSI Session + CHAP authentication + type: boolean + fsType: + description: 'Filesystem type of the volume + that you want to mount. Tip: Ensure + that the filesystem type is supported + by the host operating system. Examples: + "ext4", "xfs", "ntfs". Implicitly inferred + to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the + filesystem from compromising the machine' + type: string + initiatorName: + description: Custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface + simultaneously, new iSCSI interface + : will be + created for the connection. + type: string + iqn: + description: Target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iSCSI Interface Name that + uses an iSCSI transport. Defaults to + 'default' (tcp). + type: string + lun: + description: iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: iSCSI Target Portal List. + The portal is either an IP or ip_addr:port + if the port is other than default (typically + TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: ReadOnly here will force + the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: CHAP Secret for iSCSI target + and initiator authentication + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + targetPortal: + description: iSCSI Target Portal. The + Portal is either an IP or ip_addr:port + if the port is other than default (typically + TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'Volume''s name. Must be a DNS_LABEL + and unique within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'NFS represents an NFS mount + on the host that shares a pod''s lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'Path that is exported by + the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'ReadOnly here will force + the NFS export to be mounted with read-only + permissions. Defaults to false. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'Server is the hostname or + IP address of the NFS server. More info: + https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'PersistentVolumeClaimVolumeSource + represents a reference to a PersistentVolumeClaim + in the same namespace. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'ClaimName is the name of + a PersistentVolumeClaim in the same + namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: Will force the ReadOnly setting + in VolumeMounts. Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: PhotonPersistentDisk represents + a PhotonController persistent disk attached + and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. + Must be a filesystem type supported + by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to + be "ext4" if unspecified. + type: string + pdID: + description: ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: PortworxVolume represents a portworx + volume attached and mounted on kubelets + host machine + properties: + fsType: + description: FSType represents the filesystem + type to mount Must be a filesystem type + supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred + to be "ext4" if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). + ReadOnly here will force the ReadOnly + setting in VolumeMounts. + type: boolean + volumeID: + description: VolumeID uniquely identifies + a Portworx volume + type: string + required: + - volumeID + type: object + projected: + description: Items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: Mode bits used to set permissions + on created files by default. Must be + an octal value between 0000 and 0777 + or a decimal value between 0 and 511. + YAML accepts both octal and decimal + values, JSON requires decimal values + for mode bits. Directories within the + path are not affected by this setting. + This might be in conflict with other + options that affect the file mode, like + fsGroup, and the result can be other + mode bits set. + format: int32 + type: integer + sources: + description: list of volume projections + items: + description: Projection that may be + projected along with other supported + volume types + properties: + configMap: + description: information about the + configMap data to project + properties: + items: + description: If unspecified, + each key-value pair in the + Data field of the referenced + ConfigMap will be projected + into the volume as a file + whose name is the key and + content is the value. If specified, + the listed keys will be projected + into the specified paths, + and unlisted keys will not + be present. If a key is specified + which is not present in the + ConfigMap, the volume setup + will error unless it is marked + optional. Paths must be relative + and may not contain the '..' + path or start with '..'. + items: + description: Maps a string + key to a path within a volume. + properties: + key: + description: The key to + project. + type: string + mode: + description: 'Optional: + mode bits used to set + permissions on this + file. Must be an octal + value between 0000 and + 0777 or a decimal value + between 0 and 511. YAML + accepts both octal and + decimal values, JSON + requires decimal values + for mode bits. If not + specified, the volume + defaultMode will be + used. This might be + in conflict with other + options that affect + the file mode, like + fsGroup, and the result + can be other mode bits + set.' + format: int32 + type: integer + path: + description: The relative + path of the file to + map the key to. May + not be an absolute path. + May not contain the + path element '..'. May + not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether + the ConfigMap or its keys + must be defined + type: boolean + type: object + downwardAPI: + description: information about the + downwardAPI data to project + properties: + items: + description: Items is a list + of DownwardAPIVolume file + items: + description: DownwardAPIVolumeFile + represents information to + create the file containing + the pod field + properties: + fieldRef: + description: 'Required: + Selects a field of the + pod: only annotations, + labels, name and namespace + are supported.' + properties: + apiVersion: + description: Version + of the schema the + FieldPath is written + in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path + of the field to + select in the specified + API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: + mode bits used to set + permissions on this + file, must be an octal + value between 0000 and + 0777 or a decimal value + between 0 and 511. YAML + accepts both octal and + decimal values, JSON + requires decimal values + for mode bits. If not + specified, the volume + defaultMode will be + used. This might be + in conflict with other + options that affect + the file mode, like + fsGroup, and the result + can be other mode bits + set.' + format: int32 + type: integer + path: + description: 'Required: + Path is the relative + path name of the file + to be created. Must + not be absolute or contain + the ''..'' path. Must + be utf-8 encoded. The + first item of the relative + path must not start + with ''..''' + type: string + resourceFieldRef: + description: 'Selects + a resource of the container: + only resources limits + and requests (limits.cpu, + limits.memory, requests.cpu + and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container + name: required for + volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies + the output format + of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: + resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + description: information about the + secret data to project + properties: + items: + description: If unspecified, + each key-value pair in the + Data field of the referenced + Secret will be projected into + the volume as a file whose + name is the key and content + is the value. If specified, + the listed keys will be projected + into the specified paths, + and unlisted keys will not + be present. If a key is specified + which is not present in the + Secret, the volume setup will + error unless it is marked + optional. Paths must be relative + and may not contain the '..' + path or start with '..'. + items: + description: Maps a string + key to a path within a volume. + properties: + key: + description: The key to + project. + type: string + mode: + description: 'Optional: + mode bits used to set + permissions on this + file. Must be an octal + value between 0000 and + 0777 or a decimal value + between 0 and 511. YAML + accepts both octal and + decimal values, JSON + requires decimal values + for mode bits. If not + specified, the volume + defaultMode will be + used. This might be + in conflict with other + options that affect + the file mode, like + fsGroup, and the result + can be other mode bits + set.' + format: int32 + type: integer + path: + description: The relative + path of the file to + map the key to. May + not be an absolute path. + May not contain the + path element '..'. May + not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. + apiVersion, kind, uid?' + type: string + optional: + description: Specify whether + the Secret or its key must + be defined + type: boolean + type: object + serviceAccountToken: + description: information about the + serviceAccountToken data to project + properties: + audience: + description: Audience is the + intended audience of the token. + A recipient of a token must + identify itself with an identifier + specified in the audience + of the token, and otherwise + should reject the token. The + audience defaults to the identifier + of the apiserver. + type: string + expirationSeconds: + description: ExpirationSeconds + is the requested duration + of validity of the service + account token. As the token + approaches expiration, the + kubelet volume plugin will + proactively rotate the service + account token. The kubelet + will start trying to rotate + the token if the token is + older than 80 percent of its + time to live or if the token + is older than 24 hours.Defaults + to 1 hour and must be at least + 10 minutes. + format: int64 + type: integer + path: + description: Path is the path + relative to the mount point + of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: Quobyte represents a Quobyte + mount on the host that shares a pod's lifetime + properties: + group: + description: Group to map volume access + to Default is no group + type: string + readOnly: + description: ReadOnly here will force + the Quobyte volume to be mounted with + read-only permissions. Defaults to false. + type: boolean + registry: + description: Registry represents a single + or multiple Quobyte Registry services + specified as a string as host:port pair + (multiple entries are separated with + commas) which acts as the central registry + for volumes + type: string + tenant: + description: Tenant owning the given Quobyte + volume in the Backend Used with dynamically + provisioned Quobyte volumes, value is + set by the plugin + type: string + user: + description: User to map volume access + to Defaults to serivceaccount user + type: string + volume: + description: Volume is a string that references + an already created Quobyte volume by + name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'RBD represents a Rados Block + Device mount on the host that shares a pod''s + lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'Filesystem type of the volume + that you want to mount. Tip: Ensure + that the filesystem type is supported + by the host operating system. Examples: + "ext4", "xfs", "ntfs". Implicitly inferred + to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the + filesystem from compromising the machine' + type: string + image: + description: 'The rados image name. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'Keyring is the path to key + ring for RBDUser. Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'A collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'The rados pool name. Default + is rbd. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'ReadOnly here will force + the ReadOnly setting in VolumeMounts. + Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'SecretRef is name of the + authentication secret for RBDUser. If + provided overrides keyring. Default + is nil. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + user: + description: 'The rados user name. Default + is admin. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: ScaleIO represents a ScaleIO + persistent volume attached and mounted on + Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. + Must be a filesystem type supported + by the host operating system. Ex. "ext4", + "xfs", "ntfs". Default is "xfs". + type: string + gateway: + description: The host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: The name of the ScaleIO Protection + Domain for the configured storage. + type: string + readOnly: + description: Defaults to false (read/write). + ReadOnly here will force the ReadOnly + setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef references to the + secret for ScaleIO user and other sensitive + information. If this is not provided, + Login operation will fail. + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + sslEnabled: + description: Flag to enable/disable SSL + communication with Gateway, default + false + type: boolean + storageMode: + description: Indicates whether the storage + for a volume should be ThickProvisioned + or ThinProvisioned. Default is ThinProvisioned. + type: string + storagePool: + description: The ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: The name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: The name of a volume already + created in the ScaleIO system that is + associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'Secret represents a secret that + should populate this volume. More info: + https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'Optional: mode bits used + to set permissions on created files + by default. Must be an octal value between + 0000 and 0777 or a decimal value between + 0 and 511. YAML accepts both octal and + decimal values, JSON requires decimal + values for mode bits. Defaults to 0644. + Directories within the path are not + affected by this setting. This might + be in conflict with other options that + affect the file mode, like fsGroup, + and the result can be other mode bits + set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value + pair in the Data field of the referenced + Secret will be projected into the volume + as a file whose name is the key and + content is the value. If specified, + the listed keys will be projected into + the specified paths, and unlisted keys + will not be present. If a key is specified + which is not present in the Secret, + the volume setup will error unless it + is marked optional. Paths must be relative + and may not contain the '..' path or + start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits + used to set permissions on this + file. Must be an octal value between + 0000 and 0777 or a decimal value + between 0 and 511. YAML accepts + both octal and decimal values, + JSON requires decimal values for + mode bits. If not specified, the + volume defaultMode will be used. + This might be in conflict with + other options that affect the + file mode, like fsGroup, and the + result can be other mode bits + set.' + format: int32 + type: integer + path: + description: The relative path of + the file to map the key to. May + not be an absolute path. May not + contain the path element '..'. + May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: Specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: 'Name of the secret in the + pod''s namespace to use. More info: + https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: StorageOS represents a StorageOS + volume attached and mounted on Kubernetes + nodes. + properties: + fsType: + description: Filesystem type to mount. + Must be a filesystem type supported + by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to + be "ext4" if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). + ReadOnly here will force the ReadOnly + setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef specifies the secret + to use for obtaining the StorageOS API + credentials. If not specified, default + values will be attempted. + properties: + name: + description: 'Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, + kind, uid?' + type: string + type: object + volumeName: + description: VolumeName is the human-readable + name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: VolumeNamespace specifies + the scope of the volume within StorageOS. If + no namespace is specified then the Pod's + namespace will be used. This allows + the Kubernetes name scoping to be mirrored + within StorageOS for tighter integration. + Set VolumeName to any name to override + the default behaviour. Set to "default" + if you are not using namespaces within + StorageOS. Namespaces that do not pre-exist + within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: VsphereVolume represents a vSphere + volume attached and mounted on kubelets + host machine + properties: + fsType: + description: Filesystem type to mount. + Must be a filesystem type supported + by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to + be "ext4" if unspecified. + type: string + storagePolicyID: + description: Storage Policy Based Management + (SPBM) profile ID associated with the + StoragePolicyName. + type: string + storagePolicyName: + description: Storage Policy Based Management + (SPBM) profile name. + type: string + volumePath: + description: Path that identifies vSphere + volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - containers + type: object + type: object + required: + - selector + - template + type: object + required: + - spec + type: object + type: array + additionalServices: + items: + description: DeploymentTemplateSpec defines the pool template of + Deployment. + properties: + metadata: + type: object + spec: + description: ServiceSpec describes the attributes that a user + creates on a service. + properties: + allocateLoadBalancerNodePorts: + description: allocateLoadBalancerNodePorts defines if NodePorts + will be automatically allocated for services with type + LoadBalancer. Default is "true". It may be set to "false" + if the cluster load-balancer does not rely on NodePorts. If + the caller requests specific NodePorts (by specifying + a value), those requests will be respected, regardless + of this field. This field may only be set for services + with type LoadBalancer and will be cleared if the type + is changed to any other type. This field is beta-level + and is only honored by servers that enable the ServiceLBNodePortControl + feature. + type: boolean + clusterIP: + description: clusterIP is the IP address of the service + and is usually assigned randomly. If an address is specified + manually, is in-range (as per system configuration), and + is not in use, it will be allocated to the service; otherwise + creation of the service will fail. This field may not + be changed through updates unless the type field is also + being changed to ExternalName (which requires this field + to be blank) or the type field is being changed from ExternalName + (in which case this field may optionally be specified, + as describe above). Valid values are "None", empty string + (""), or a valid IP address. Setting this to "None" makes + a "headless service" (no virtual IP), which is useful + when direct endpoint connections are preferred and proxying + is not required. Only applies to types ClusterIP, NodePort, + and LoadBalancer. If this field is specified when creating + a Service of type ExternalName, creation will fail. This + field will be wiped when updating a Service to type ExternalName. + type: string + clusterIPs: + description: ClusterIPs is a list of IP addresses assigned + to this service, and are usually assigned randomly. If + an address is specified manually, is in-range (as per + system configuration), and is not in use, it will be allocated + to the service; otherwise creation of the service will + fail. This field may not be changed through updates unless + the type field is also being changed to ExternalName (which + requires this field to be empty) or the type field is + being changed from ExternalName (in which case this field + may optionally be specified, as describe above). Valid + values are "None", empty string (""), or a valid IP address. Setting + this to "None" makes a "headless service" (no virtual + IP), which is useful when direct endpoint connections + are preferred and proxying is not required. Only applies + to types ClusterIP, NodePort, and LoadBalancer. If this + field is specified when creating a Service of type ExternalName, + creation will fail. + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + description: externalIPs is a list of IP addresses for which + nodes in the cluster will also accept traffic for this + service. These IPs are not managed by Kubernetes. The + user is responsible for ensuring that traffic arrives + at a node with this IP. A common example is external + load-balancers that are not part of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that + discovery mechanisms will return as an alias for this + service (e.g. a DNS CNAME record). No proxying will be + involved. Must be a lowercase RFC-1123 hostname (https://tools.ietf.org/html/rfc1123) + and requires `type` to be "ExternalName". + type: string + externalTrafficPolicy: + description: externalTrafficPolicy denotes if this Service + desires to route external traffic to node-local or cluster-wide + endpoints. "Local" preserves the client source IP and + avoids a second hop for LoadBalancer and Nodeport type + services, but risks potentially imbalanced traffic spreading. + "Cluster" obscures the client source IP and may cause + a second hop to another node, but should have good overall + load-spreading. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck + nodePort for the service. This only applies when type + is set to LoadBalancer and externalTrafficPolicy is set + to Local. If a value is specified, is in-range, and is + not in use, it will be used. If not specified, a value + will be automatically allocated. External systems (e.g. + load-balancers) can use this port to determine if a given + node holds endpoints for this service or not. If this + field is specified when creating a Service which does + not need it, creation will fail. This field will be wiped + when updating a Service to no longer need it (e.g. changing + type). + format: int32 + type: integer + internalTrafficPolicy: + description: InternalTrafficPolicy specifies if the cluster + internal traffic should be routed to all endpoints or + node-local endpoints only. "Cluster" routes internal traffic + to a Service to all endpoints. "Local" routes traffic + to node-local endpoints only, traffic is dropped if no + node-local endpoints are ready. The default value is "Cluster". + type: string + ipFamilies: + description: "IPFamilies is a list of IP families (e.g. + IPv4, IPv6) assigned to this service, and is gated by + the \"IPv6DualStack\" feature gate. This field is usually + assigned automatically based on cluster configuration + and the ipFamilyPolicy field. If this field is specified + manually, the requested family is available in the cluster, + and ipFamilyPolicy allows it, it will be used; otherwise + creation of the service will fail. This field is conditionally + mutable: it allows for adding or removing a secondary + IP family, but it does not allow changing the primary + IP family of the Service. Valid values are \"IPv4\" and + \"IPv6\". This field only applies to Services of types + ClusterIP, NodePort, and LoadBalancer, and does apply + to \"headless\" services. This field will be wiped when + updating a Service to type ExternalName. \n This field + may hold a maximum of two entries (dual-stack families, + in either order). These families must correspond to the + values of the clusterIPs field, if specified." + items: + description: IPFamily represents the IP Family (IPv4 or + IPv6). This type is used to express the family of an + IP expressed by a type (e.g. service.spec.ipFamilies). + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + description: IPFamilyPolicy represents the dual-stack-ness + requested or required by this Service, and is gated by + the "IPv6DualStack" feature gate. If there is no value + provided, then this field will be set to SingleStack. + Services can be "SingleStack" (a single IP family), "PreferDualStack" + (two IP families on dual-stack configured clusters or + a single IP family on single-stack clusters), or "RequireDualStack" + (two IP families on dual-stack configured clusters, otherwise + fail). The ipFamilies and clusterIPs fields depend on + the value of this field. This field will be wiped when + updating a service to type ExternalName. + type: string + loadBalancerClass: + description: loadBalancerClass is the class of the load + balancer implementation this Service belongs to. If specified, + the value of this field must be a label-style identifier, + with an optional prefix, e.g. "internal-vip" or "example.com/internal-vip". + Unprefixed names are reserved for end-users. This field + can only be set when the Service type is 'LoadBalancer'. + If not set, the default load balancer implementation is + used, today this is typically done through the cloud provider + integration, but should apply for any default implementation. + If set, it is assumed that a load balancer implementation + is watching for Services with a matching class. Any default + load balancer implementation (e.g. cloud providers) should + ignore Services that set this field. This field can only + be set when creating or updating a Service to type 'LoadBalancer'. + Once set, it can not be changed. This field will be wiped + when a service is updated to a non 'LoadBalancer' type. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer + LoadBalancer will get created with the IP specified in + this field. This feature depends on whether the underlying + cloud-provider supports specifying the loadBalancerIP + when a load balancer is created. This field will be ignored + if the cloud-provider does not support the feature.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, + this will restrict traffic through the cloud-provider + load-balancer will be restricted to the specified client + IPs. This field will be ignored if the cloud-provider + does not support the feature." More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this + service. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's + port. + properties: + appProtocol: + description: The application protocol for this port. + This field follows standard Kubernetes label syntax. + Un-prefixed names are reserved for IANA standard + service names (as per RFC-6335 and http://www.iana.org/assignments/service-names). + Non-standard protocols should use prefixed names + such as mycompany.com/my-custom-protocol. + type: string + name: + description: The name of this port within the service. + This must be a DNS_LABEL. All ports within a ServiceSpec + must have unique names. When considering the endpoints + for a Service, this must match the 'name' field + in the EndpointPort. Optional if only one ServicePort + is defined on this service. + type: string + nodePort: + description: 'The port on each node on which this + service is exposed when type is NodePort or LoadBalancer. Usually + assigned by the system. If a value is specified, + in-range, and not in use it will be used, otherwise + the operation will fail. If not specified, a port + will be allocated if this Service requires one. If + this field is specified when creating a Service + which does not need it, creation will fail. This + field will be wiped when updating a Service to no + longer need it (e.g. changing type from NodePort + to ClusterIP). More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this + service. + format: int32 + type: integer + protocol: + default: TCP + description: The IP protocol for this port. Supports + "TCP", "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access + on the pods targeted by the service. Number must + be in the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named + port in the target Pod''s container ports. If this + is not specified, the value of the ''port'' field + is used (an identity map). This field is ignored + for services with clusterIP=None, and should be + omitted or set equal to the ''port'' field. More + info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + description: publishNotReadyAddresses indicates that any + agent which deals with endpoints for this Service should + disregard any indications of ready/not-ready. The primary + use case for setting this field is for a StatefulSet's + Headless Service to propagate SRV DNS records for its + Pods for the purpose of peer discovery. The Kubernetes + controllers that generate Endpoints and EndpointSlice + resources for Services interpret this to mean that all + endpoints are considered "ready" even if the Pods themselves + are not. Agents which consume only Kubernetes generated + endpoints through the Endpoints or EndpointSlice resources + can safely assume this behavior. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys + and values matching this selector. If empty or not present, + the service is assumed to have an external process managing + its endpoints, which Kubernetes will not modify. Only + applies to types ClusterIP, NodePort, and LoadBalancer. + Ignored if type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. + Must be ClientIP or None. Defaults to None. More info: + https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of + Client IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds + of ClientIP type session sticky time. The value + must be >0 && <=86400(for 1 day) if ServiceAffinity + == "ClientIP". Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. + Defaults to ClusterIP. Valid options are ExternalName, + ClusterIP, NodePort, and LoadBalancer. "ClusterIP" allocates + a cluster-internal IP address for load-balancing to endpoints. + Endpoints are determined by the selector or if that is + not specified, by manual construction of an Endpoints + object or EndpointSlice objects. If clusterIP is "None", + no virtual IP is allocated and the endpoints are published + as a set of endpoints rather than a virtual IP. "NodePort" + builds on ClusterIP and allocates a port on every node + which routes to the same endpoints as the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer + (if supported in the current cloud) which routes to the + same endpoints as the clusterIP. "ExternalName" aliases + this service to the specified externalName. Several other + fields do not apply to ExternalName services. More info: + https://kubernetes.' + type: string + type: object + required: + - spec + type: object + type: array + imageRegistry: + type: string + poolName: + type: string + serviceType: + description: Service Type string describes ingress methods for a service + type: string + version: + type: string + type: object + status: + description: PlatformAdminStatus defines the observed state of PlatformAdmin + properties: + conditions: + description: Current PlatformAdmin state + items: + description: PlatformAdminCondition describes current state of a + PlatformAdmin. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of in place set condition. + type: string + type: object + type: array + deploymentReadyReplicas: + format: int32 + type: integer + deploymentReplicas: + format: int32 + type: integer + initialized: + type: boolean + ready: + type: boolean + serviceReadyReplicas: + format: int32 + type: integer + serviceReplicas: + format: int32 + type: integer + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The platformadmin ready status + jsonPath: .status.ready + name: READY + type: boolean + - description: The Ready Component. + jsonPath: .status.readyComponentNum + name: ReadyComponentNum + type: integer + - description: The Unready Component. + jsonPath: .status.unreadyComponentNum + name: UnreadyComponentNum + type: integer + name: v1alpha2 + schema: + openAPIV3Schema: + description: PlatformAdmin is the Schema for the samples API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PlatformAdminSpec defines the desired state of PlatformAdmin + properties: + components: + items: + description: Component defines the components of EdgeX + properties: + image: + type: string + name: + type: string + required: + - name + type: object + type: array + imageRegistry: + type: string + platform: + type: string + poolName: + type: string + security: + type: boolean + version: + type: string + type: object + status: + description: PlatformAdminStatus defines the observed state of PlatformAdmin + properties: + conditions: + description: Current PlatformAdmin state + items: + description: PlatformAdminCondition describes current state of a + PlatformAdmin. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of in place set condition. + type: string + type: object + type: array + initialized: + type: boolean + ready: + type: boolean + readyComponentNum: + format: int32 + type: integer + unreadyComponentNum: + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index 70435df84b4..9475ecc21f7 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -255,6 +255,28 @@ rules: - patch - update - watch +- apiGroups: + - "" + resources: + - configmaps + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - configmaps/status + - services/status + verbs: + - get + - patch + - update - apiGroups: - "" resources: @@ -360,6 +382,32 @@ rules: - list - patch - watch +- apiGroups: + - iot.openyurt.io + resources: + - platformadmins + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - iot.openyurt.io + resources: + - platformadmins/finalizers + verbs: + - update +- apiGroups: + - iot.openyurt.io + resources: + - platformadmins/status + verbs: + - get + - patch + - update - apiGroups: - raven.openyurt.io resources: @@ -430,6 +478,26 @@ webhooks: resources: - nodepools sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: yurt-manager-webhook-service + namespace: {{ .Release.Namespace }} + path: /mutate-iot-openyurt-io-v1alpha2-platformadmin + failurePolicy: Fail + name: mplatformadmin.kb.io + rules: + - apiGroups: + - iot.openyurt.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - platformadmins + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 @@ -541,6 +609,26 @@ webhooks: resources: - nodepools sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: yurt-manager-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-iot-openyurt-io-v1alpha2-platformadmin + failurePolicy: Fail + name: vplatformadmin.kb.io + rules: + - apiGroups: + - iot.openyurt.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - platformadmins + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/cmd/yurt-manager/app/options/options.go b/cmd/yurt-manager/app/options/options.go index 0ae5bb68304..2ed09217543 100644 --- a/cmd/yurt-manager/app/options/options.go +++ b/cmd/yurt-manager/app/options/options.go @@ -31,6 +31,7 @@ type YurtManagerOptions struct { YurtStaticSetController *YurtStaticSetControllerOptions YurtAppSetController *YurtAppSetControllerOptions YurtAppDaemonController *YurtAppDaemonControllerOptions + PlatformAdminController *PlatformAdminControllerOptions } // NewYurtManagerOptions creates a new YurtManagerOptions with a default config. @@ -43,6 +44,7 @@ func NewYurtManagerOptions() (*YurtManagerOptions, error) { YurtStaticSetController: NewYurtStaticSetControllerOptions(), YurtAppSetController: NewYurtAppSetControllerOptions(), YurtAppDaemonController: NewYurtAppDaemonControllerOptions(), + PlatformAdminController: NewPlatformAdminControllerOptions(), } return &s, nil @@ -55,6 +57,7 @@ func (y *YurtManagerOptions) Flags() cliflag.NamedFlagSets { y.GatewayController.AddFlags(fss.FlagSet("gateway controller")) y.YurtStaticSetController.AddFlags(fss.FlagSet("yurtstaticset controller")) y.YurtAppDaemonController.AddFlags(fss.FlagSet("yurtappdaemon controller")) + y.PlatformAdminController.AddFlags(fss.FlagSet("iot controller")) // Please Add Other controller flags @kadisi return fss @@ -68,6 +71,7 @@ func (y *YurtManagerOptions) Validate() error { errs = append(errs, y.GatewayController.Validate()...) errs = append(errs, y.YurtStaticSetController.Validate()...) errs = append(errs, y.YurtAppDaemonController.Validate()...) + errs = append(errs, y.PlatformAdminController.Validate()...) return utilerrors.NewAggregate(errs) } @@ -85,6 +89,9 @@ func (y *YurtManagerOptions) ApplyTo(c *config.Config) error { if err := y.YurtAppDaemonController.ApplyTo(&c.ComponentConfig.YurtAppDaemonController); err != nil { return err } + if err := y.PlatformAdminController.ApplyTo(&c.ComponentConfig.PlatformAdminController); err != nil { + return err + } return nil } diff --git a/cmd/yurt-manager/app/options/platformadmincontroller.go b/cmd/yurt-manager/app/options/platformadmincontroller.go new file mode 100644 index 00000000000..ce8aa99e682 --- /dev/null +++ b/cmd/yurt-manager/app/options/platformadmincontroller.go @@ -0,0 +1,63 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + "errors" + + "github.com/spf13/pflag" + + "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" +) + +type PlatformAdminControllerOptions struct { + *config.PlatformAdminControllerConfiguration +} + +func NewPlatformAdminControllerOptions() *PlatformAdminControllerOptions { + return &PlatformAdminControllerOptions{ + config.NewPlatformAdminControllerConfiguration(), + } +} + +// AddFlags adds flags related to nodepool for yurt-manager to the specified FlagSet. +func (n *PlatformAdminControllerOptions) AddFlags(fs *pflag.FlagSet) { + if n == nil { + return + } +} + +// ApplyTo fills up nodepool config with options. +func (o *PlatformAdminControllerOptions) ApplyTo(cfg *config.PlatformAdminControllerConfiguration) error { + if o == nil { + return nil + } + *cfg = *o.PlatformAdminControllerConfiguration + return nil +} + +// Validate checks validation of IoTControllerOptions. +func (o *PlatformAdminControllerOptions) Validate() []error { + if o == nil { + return nil + } + errs := []error{} + if o.PlatformAdminControllerConfiguration == nil { + errs = append(errs, errors.New("IoTControllerConfiguration can not be empty!")) + } + return errs +} diff --git a/go.mod b/go.mod index 5ff4c8b15d2..e938844ec97 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( google.golang.org/grpc v1.55.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/square/go-jose.v2 v2.6.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.22.3 k8s.io/apimachinery v0.22.3 k8s.io/apiserver v0.22.3 @@ -153,7 +154,6 @@ require ( gopkg.in/ini.v1 v1.66.2 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.22.2 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22 // indirect diff --git a/pkg/apis/addtoscheme_iot_v1alpha1.go b/pkg/apis/addtoscheme_iot_v1alpha1.go new file mode 100644 index 00000000000..99eda0003ab --- /dev/null +++ b/pkg/apis/addtoscheme_iot_v1alpha1.go @@ -0,0 +1,26 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apis + +import ( + version "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, version.SchemeBuilder.AddToScheme) +} diff --git a/pkg/apis/addtoscheme_iot_v1alpha2.go b/pkg/apis/addtoscheme_iot_v1alpha2.go new file mode 100644 index 00000000000..a42e086bb61 --- /dev/null +++ b/pkg/apis/addtoscheme_iot_v1alpha2.go @@ -0,0 +1,26 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apis + +import ( + version "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, version.SchemeBuilder.AddToScheme) +} diff --git a/pkg/apis/iot/v1alpha1/condition_const.go b/pkg/apis/iot/v1alpha1/condition_const.go new file mode 100644 index 00000000000..7f1c8b5e84e --- /dev/null +++ b/pkg/apis/iot/v1alpha1/condition_const.go @@ -0,0 +1,38 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +const ( + // ConfigmapAvailableCondition documents the status of the PlatformAdmin configmap. + ConfigmapAvailableCondition PlatformAdminConditionType = "ConfigmapAvailable" + + ConfigmapProvisioningReason = "ConfigmapProvisioning" + + ConfigmapProvisioningFailedReason = "ConfigmapProvisioningFailed" + // ServiceAvailableCondition documents the status of the PlatformAdmin service. + ServiceAvailableCondition PlatformAdminConditionType = "ServiceAvailable" + + ServiceProvisioningReason = "ServiceProvisioning" + + ServiceProvisioningFailedReason = "ServiceProvisioningFailed" + // DeploymentAvailableCondition documents the status of the PlatformAdmin deployment. + DeploymentAvailableCondition PlatformAdminConditionType = "DeploymentAvailable" + + DeploymentProvisioningReason = "DeploymentProvisioning" + + DeploymentProvisioningFailedReason = "DeploymentProvisioningFailed" +) diff --git a/pkg/apis/iot/v1alpha1/default.go b/pkg/apis/iot/v1alpha1/default.go new file mode 100644 index 00000000000..6f162a97f5e --- /dev/null +++ b/pkg/apis/iot/v1alpha1/default.go @@ -0,0 +1,24 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +// SetDefaultsPlatformAdmin set default values for PlatformAdmin. +func SetDefaultsPlatformAdmin(obj *PlatformAdmin) { + if obj.Annotations == nil { + obj.Annotations = make(map[string]string) + } +} diff --git a/pkg/apis/iot/v1alpha1/doc.go b/pkg/apis/iot/v1alpha1/doc.go new file mode 100644 index 00000000000..cab0150b22b --- /dev/null +++ b/pkg/apis/iot/v1alpha1/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// +groupName=iot.openyurt.io +package v1alpha1 diff --git a/pkg/apis/iot/v1alpha1/groupversion_info.go b/pkg/apis/iot/v1alpha1/groupversion_info.go new file mode 100644 index 00000000000..11ab27eb03b --- /dev/null +++ b/pkg/apis/iot/v1alpha1/groupversion_info.go @@ -0,0 +1,44 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +// Package v1alpha1 contains API Schema definitions for the device v1alpha1API group +// +kubebuilder:object:generate=true +// +groupName=iot.openyurt.io + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "iot.openyurt.io", Version: "v1alpha1"} + + SchemeGroupVersion = GroupVersion + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Resource is required by pkg/client/listers/... +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/iot/v1alpha1/platformadmin_conversion.go b/pkg/apis/iot/v1alpha1/platformadmin_conversion.go new file mode 100644 index 00000000000..1736ca18d21 --- /dev/null +++ b/pkg/apis/iot/v1alpha1/platformadmin_conversion.go @@ -0,0 +1,141 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "encoding/json" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/conversion" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" +) + +func (src *PlatformAdmin) ConvertTo(dstRaw conversion.Hub) error { + // Transform metadata + dst := dstRaw.(*v1alpha2.PlatformAdmin) + dst.ObjectMeta = src.ObjectMeta + dst.TypeMeta = src.TypeMeta + dst.TypeMeta.APIVersion = "iot.openyurt.io/v1alpha2" + + // Transform spec + dst.Spec.Version = src.Spec.Version + dst.Spec.Security = false + dst.Spec.ImageRegistry = src.Spec.ImageRegistry + dst.Spec.PoolName = src.Spec.PoolName + dst.Spec.Platform = v1alpha2.PlatformAdminPlatformEdgeX + + // Transform status + dst.Status.Ready = src.Status.Ready + dst.Status.Initialized = src.Status.Initialized + dst.Status.ReadyComponentNum = src.Status.DeploymentReadyReplicas + dst.Status.UnreadyComponentNum = src.Status.DeploymentReplicas - src.Status.DeploymentReadyReplicas + dst.Status.Conditions = transToV2Condition(src.Status.Conditions) + + // Transform additionaldeployment + if len(src.Spec.AdditionalDeployment) > 0 { + additionalDeployment, err := json.Marshal(src.Spec.AdditionalDeployment) + if err != nil { + return err + } + dst.ObjectMeta.Annotations["AdditionalDeployments"] = string(additionalDeployment) + } + + // Transform additionalservice + if len(src.Spec.AdditionalService) > 0 { + additionalService, err := json.Marshal(src.Spec.AdditionalService) + if err != nil { + return err + } + dst.ObjectMeta.Annotations["AdditionalServices"] = string(additionalService) + } + + //TODO: Components + + return nil +} + +func (dst *PlatformAdmin) ConvertFrom(srcRaw conversion.Hub) error { + // Transform metadata + src := srcRaw.(*v1alpha2.PlatformAdmin) + dst.ObjectMeta = src.ObjectMeta + dst.TypeMeta = src.TypeMeta + dst.TypeMeta.APIVersion = "iot.openyurt.io/v1alpha1" + + // Transform spec + dst.Spec.Version = src.Spec.Version + dst.Spec.ImageRegistry = src.Spec.ImageRegistry + dst.Spec.PoolName = src.Spec.PoolName + dst.Spec.ServiceType = corev1.ServiceTypeClusterIP + + // Transform status + dst.Status.Ready = src.Status.Ready + dst.Status.Initialized = src.Status.Initialized + dst.Status.ServiceReadyReplicas = src.Status.ReadyComponentNum + dst.Status.ServiceReplicas = src.Status.ReadyComponentNum + src.Status.UnreadyComponentNum + dst.Status.DeploymentReadyReplicas = src.Status.ReadyComponentNum + dst.Status.DeploymentReplicas = src.Status.ReadyComponentNum + src.Status.UnreadyComponentNum + dst.Status.Conditions = transToV1Condition(src.Status.Conditions) + + // Transform additionaldeployment + if _, ok := src.ObjectMeta.Annotations["AdditionalDeployments"]; ok { + var additionalDeployments []DeploymentTemplateSpec = make([]DeploymentTemplateSpec, 0) + err := json.Unmarshal([]byte(src.ObjectMeta.Annotations["AdditionalDeployments"]), &additionalDeployments) + if err != nil { + return err + } + dst.Spec.AdditionalDeployment = additionalDeployments + } + + // Transform additionalservice + if _, ok := src.ObjectMeta.Annotations["AdditionalServices"]; ok { + var additionalServices []ServiceTemplateSpec = make([]ServiceTemplateSpec, 0) + err := json.Unmarshal([]byte(src.ObjectMeta.Annotations["AdditionalServices"]), &additionalServices) + if err != nil { + return err + } + dst.Spec.AdditionalService = additionalServices + } + + return nil +} + +func transToV1Condition(c2 []v1alpha2.PlatformAdminCondition) (c1 []PlatformAdminCondition) { + for _, ic := range c2 { + c1 = append(c1, PlatformAdminCondition{ + Type: PlatformAdminConditionType(ic.Type), + Status: ic.Status, + LastTransitionTime: ic.LastTransitionTime, + Reason: ic.Reason, + Message: ic.Message, + }) + } + return +} + +func transToV2Condition(c1 []PlatformAdminCondition) (c2 []v1alpha2.PlatformAdminCondition) { + for _, ic := range c1 { + c2 = append(c2, v1alpha2.PlatformAdminCondition{ + Type: v1alpha2.PlatformAdminConditionType(ic.Type), + Status: ic.Status, + LastTransitionTime: ic.LastTransitionTime, + Reason: ic.Reason, + Message: ic.Message, + }) + } + return +} diff --git a/pkg/apis/iot/v1alpha1/platformadmin_types.go b/pkg/apis/iot/v1alpha1/platformadmin_types.go new file mode 100644 index 00000000000..b32d110beff --- /dev/null +++ b/pkg/apis/iot/v1alpha1/platformadmin_types.go @@ -0,0 +1,142 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // name of finalizer + EdgexFinalizer = "edgex.edgexfoundry.org" + + LabelEdgeXGenerate = "www.edgexfoundry.org/generate" +) + +// PlatformAdminConditionType indicates valid conditions type of a iot platform. +type PlatformAdminConditionType string +type PlatformAdminConditionSeverity string + +// DeploymentTemplateSpec defines the pool template of Deployment. +type DeploymentTemplateSpec struct { + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec appsv1.DeploymentSpec `json:"spec"` +} + +// DeploymentTemplateSpec defines the pool template of Deployment. +type ServiceTemplateSpec struct { + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec corev1.ServiceSpec `json:"spec"` +} + +// PlatformAdminSpec defines the desired state of PlatformAdmin +type PlatformAdminSpec struct { + Version string `json:"version,omitempty"` + + ImageRegistry string `json:"imageRegistry,omitempty"` + + PoolName string `json:"poolName,omitempty"` + + ServiceType corev1.ServiceType `json:"serviceType,omitempty"` + // +optional + AdditionalService []ServiceTemplateSpec `json:"additionalServices,omitempty"` + + // +optional + AdditionalDeployment []DeploymentTemplateSpec `json:"additionalDeployments,omitempty"` +} + +// PlatformAdminStatus defines the observed state of PlatformAdmin +type PlatformAdminStatus struct { + // +optional + Ready bool `json:"ready,omitempty"` + // +optional + Initialized bool `json:"initialized,omitempty"` + // +optional + ServiceReplicas int32 `json:"serviceReplicas,omitempty"` + // +optional + ServiceReadyReplicas int32 `json:"serviceReadyReplicas,omitempty"` + // +optional + DeploymentReplicas int32 `json:"deploymentReplicas,omitempty"` + // +optional + DeploymentReadyReplicas int32 `json:"deploymentReadyReplicas,omitempty"` + + // Current PlatformAdmin state + // +optional + Conditions []PlatformAdminCondition `json:"conditions,omitempty"` +} + +// PlatformAdminCondition describes current state of a PlatformAdmin. +type PlatformAdminCondition struct { + // Type of in place set condition. + Type PlatformAdminConditionType `json:"type,omitempty"` + + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status,omitempty"` + + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,path=platformadmins,shortName=pa,categories=all +// +kubebuilder:printcolumn:name="READY",type="boolean",JSONPath=".status.ready",description="The platform ready status" +// +kubebuilder:printcolumn:name="Service",type="integer",JSONPath=".status.serviceReplicas",description="The Service Replica." +// +kubebuilder:printcolumn:name="ReadyService",type="integer",JSONPath=".status.serviceReadyReplicas",description="The Ready Service Replica." +// +kubebuilder:printcolumn:name="Deployment",type="integer",JSONPath=".status.deploymentReplicas",description="The Deployment Replica." +// +kubebuilder:printcolumn:name="ReadyDeployment",type="integer",JSONPath=".status.deploymentReadyReplicas",description="The Ready Deployment Replica." +// +kubebuilder:deprecatedversion:warning="iot.openyurt.io/v1alpha1 PlatformAdmin will be deprecated in future; use iot.openyurt.io/v1alpha2 PlatformAdmin; v1alpha1 PlatformAdmin.Spec.ServiceType only support ClusterIP" + +// PlatformAdmin is the Schema for the samples API +type PlatformAdmin struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PlatformAdminSpec `json:"spec,omitempty"` + Status PlatformAdminStatus `json:"status,omitempty"` +} + +func (c *PlatformAdmin) GetConditions() []PlatformAdminCondition { + return c.Status.Conditions +} + +func (c *PlatformAdmin) SetConditions(conditions []PlatformAdminCondition) { + c.Status.Conditions = conditions +} + +//+kubebuilder:object:root=true + +// PlatformAdminList contains a list of PlatformAdmin +type PlatformAdminList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PlatformAdmin `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PlatformAdmin{}, &PlatformAdminList{}) +} diff --git a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..62aaa062a52 --- /dev/null +++ b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,186 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentTemplateSpec) DeepCopyInto(out *DeploymentTemplateSpec) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentTemplateSpec. +func (in *DeploymentTemplateSpec) DeepCopy() *DeploymentTemplateSpec { + if in == nil { + return nil + } + out := new(DeploymentTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdmin) DeepCopyInto(out *PlatformAdmin) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdmin. +func (in *PlatformAdmin) DeepCopy() *PlatformAdmin { + if in == nil { + return nil + } + out := new(PlatformAdmin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlatformAdmin) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdminCondition) DeepCopyInto(out *PlatformAdminCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdminCondition. +func (in *PlatformAdminCondition) DeepCopy() *PlatformAdminCondition { + if in == nil { + return nil + } + out := new(PlatformAdminCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdminList) DeepCopyInto(out *PlatformAdminList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PlatformAdmin, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdminList. +func (in *PlatformAdminList) DeepCopy() *PlatformAdminList { + if in == nil { + return nil + } + out := new(PlatformAdminList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlatformAdminList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdminSpec) DeepCopyInto(out *PlatformAdminSpec) { + *out = *in + if in.AdditionalService != nil { + in, out := &in.AdditionalService, &out.AdditionalService + *out = make([]ServiceTemplateSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AdditionalDeployment != nil { + in, out := &in.AdditionalDeployment, &out.AdditionalDeployment + *out = make([]DeploymentTemplateSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdminSpec. +func (in *PlatformAdminSpec) DeepCopy() *PlatformAdminSpec { + if in == nil { + return nil + } + out := new(PlatformAdminSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdminStatus) DeepCopyInto(out *PlatformAdminStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]PlatformAdminCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdminStatus. +func (in *PlatformAdminStatus) DeepCopy() *PlatformAdminStatus { + if in == nil { + return nil + } + out := new(PlatformAdminStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceTemplateSpec) DeepCopyInto(out *ServiceTemplateSpec) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTemplateSpec. +func (in *ServiceTemplateSpec) DeepCopy() *ServiceTemplateSpec { + if in == nil { + return nil + } + out := new(ServiceTemplateSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/apis/iot/v1alpha2/condition_const.go b/pkg/apis/iot/v1alpha2/condition_const.go new file mode 100644 index 00000000000..55082cde8d6 --- /dev/null +++ b/pkg/apis/iot/v1alpha2/condition_const.go @@ -0,0 +1,32 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +const ( + // ConfigmapAvailableCondition documents the status of the PlatformAdmin configmap. + ConfigmapAvailableCondition PlatformAdminConditionType = "ConfigmapAvailable" + + ConfigmapProvisioningReason = "ConfigmapProvisioning" + + ConfigmapProvisioningFailedReason = "ConfigmapProvisioningFailed" + // ComponentAvailableCondition documents the status of the PlatformAdmin component. + ComponentAvailableCondition PlatformAdminConditionType = "ComponentAvailable" + + ComponentProvisioningReason = "ComponentProvisioning" + + ComponentProvisioningFailedReason = "ComponentProvisioningFailed" +) diff --git a/pkg/apis/iot/v1alpha2/default.go b/pkg/apis/iot/v1alpha2/default.go new file mode 100644 index 00000000000..6acc6687d5c --- /dev/null +++ b/pkg/apis/iot/v1alpha2/default.go @@ -0,0 +1,24 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +// SetDefaultsPlatformAdmin set default values for PlatformAdmin. +func SetDefaultsPlatformAdmin(obj *PlatformAdmin) { + if obj.Annotations == nil { + obj.Annotations = make(map[string]string) + } +} diff --git a/pkg/apis/iot/v1alpha2/doc.go b/pkg/apis/iot/v1alpha2/doc.go new file mode 100644 index 00000000000..63cdfa0edfe --- /dev/null +++ b/pkg/apis/iot/v1alpha2/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// +groupName=iot.openyurt.io +package v1alpha2 diff --git a/pkg/apis/iot/v1alpha2/groupversion_info.go b/pkg/apis/iot/v1alpha2/groupversion_info.go new file mode 100644 index 00000000000..2defc95d637 --- /dev/null +++ b/pkg/apis/iot/v1alpha2/groupversion_info.go @@ -0,0 +1,44 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +// Package v1alpha2 contains API Schema definitions for the device v1alpha2API group +// +kubebuilder:object:generate=true +// +groupName=iot.openyurt.io + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "iot.openyurt.io", Version: "v1alpha2"} + + SchemeGroupVersion = GroupVersion + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Resource is required by pkg/client/listers/... +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/iot/v1alpha2/platformadmin_conversion.go b/pkg/apis/iot/v1alpha2/platformadmin_conversion.go new file mode 100644 index 00000000000..fb18ecb1bea --- /dev/null +++ b/pkg/apis/iot/v1alpha2/platformadmin_conversion.go @@ -0,0 +1,20 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +// Hub marks this type as a conversion hub. +func (*PlatformAdmin) Hub() {} diff --git a/pkg/apis/iot/v1alpha2/platformadmin_types.go b/pkg/apis/iot/v1alpha2/platformadmin_types.go new file mode 100644 index 00000000000..92114e58cde --- /dev/null +++ b/pkg/apis/iot/v1alpha2/platformadmin_types.go @@ -0,0 +1,141 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // name of finalizer + PlatformAdminFinalizer = "iot.openyurt.io" + + LabelPlatformAdminGenerate = "iot.openyurt.io/generate" +) + +// PlatformAdmin platform supported by openyurt +const ( + PlatformAdminPlatformEdgeX = "edgex" +) + +// PlatformAdminConditionType indicates valid conditions type of a PlatformAdmin. +type PlatformAdminConditionType string +type PlatformAdminConditionSeverity string + +// Component defines the components of EdgeX +type Component struct { + Name string `json:"name"` + + // +optional + Image string `json:"image,omitempty"` +} + +// PlatformAdminSpec defines the desired state of PlatformAdmin +type PlatformAdminSpec struct { + Version string `json:"version,omitempty"` + + ImageRegistry string `json:"imageRegistry,omitempty"` + + PoolName string `json:"poolName,omitempty"` + + // +optional + Platform string `json:"platform,omitempty"` + + // +optional + Components []Component `json:"components,omitempty"` + + // +optional + Security bool `json:"security,omitempty"` +} + +// PlatformAdminStatus defines the observed state of PlatformAdmin +type PlatformAdminStatus struct { + // +optional + Ready bool `json:"ready,omitempty"` + + // +optional + Initialized bool `json:"initialized,omitempty"` + + // +optional + ReadyComponentNum int32 `json:"readyComponentNum,omitempty"` + + // +optional + UnreadyComponentNum int32 `json:"unreadyComponentNum,omitempty"` + + // Current PlatformAdmin state + // +optional + Conditions []PlatformAdminCondition `json:"conditions,omitempty"` +} + +// PlatformAdminCondition describes current state of a PlatformAdmin. +type PlatformAdminCondition struct { + // Type of in place set condition. + Type PlatformAdminConditionType `json:"type,omitempty"` + + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status,omitempty"` + + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced,path=platformadmins,shortName=pa,categories=all +// +kubebuilder:printcolumn:name="READY",type="boolean",JSONPath=".status.ready",description="The platformadmin ready status" +// +kubebuilder:printcolumn:name="ReadyComponentNum",type="integer",JSONPath=".status.readyComponentNum",description="The Ready Component." +// +kubebuilder:printcolumn:name="UnreadyComponentNum",type="integer",JSONPath=".status.unreadyComponentNum",description="The Unready Component." +// +kubebuilder:storageversion + +// PlatformAdmin is the Schema for the samples API +type PlatformAdmin struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PlatformAdminSpec `json:"spec,omitempty"` + Status PlatformAdminStatus `json:"status,omitempty"` +} + +func (c *PlatformAdmin) GetConditions() []PlatformAdminCondition { + return c.Status.Conditions +} + +func (c *PlatformAdmin) SetConditions(conditions []PlatformAdminCondition) { + c.Status.Conditions = conditions +} + +//+kubebuilder:object:root=true + +// PlatformAdminList contains a list of PlatformAdmin +type PlatformAdminList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PlatformAdmin `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PlatformAdmin{}, &PlatformAdminList{}) +} diff --git a/pkg/apis/iot/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/iot/v1alpha2/zz_generated.deepcopy.go new file mode 100644 index 00000000000..5b6b451488c --- /dev/null +++ b/pkg/apis/iot/v1alpha2/zz_generated.deepcopy.go @@ -0,0 +1,158 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Component) DeepCopyInto(out *Component) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Component. +func (in *Component) DeepCopy() *Component { + if in == nil { + return nil + } + out := new(Component) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdmin) DeepCopyInto(out *PlatformAdmin) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdmin. +func (in *PlatformAdmin) DeepCopy() *PlatformAdmin { + if in == nil { + return nil + } + out := new(PlatformAdmin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlatformAdmin) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdminCondition) DeepCopyInto(out *PlatformAdminCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdminCondition. +func (in *PlatformAdminCondition) DeepCopy() *PlatformAdminCondition { + if in == nil { + return nil + } + out := new(PlatformAdminCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdminList) DeepCopyInto(out *PlatformAdminList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PlatformAdmin, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdminList. +func (in *PlatformAdminList) DeepCopy() *PlatformAdminList { + if in == nil { + return nil + } + out := new(PlatformAdminList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlatformAdminList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdminSpec) DeepCopyInto(out *PlatformAdminSpec) { + *out = *in + if in.Components != nil { + in, out := &in.Components, &out.Components + *out = make([]Component, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdminSpec. +func (in *PlatformAdminSpec) DeepCopy() *PlatformAdminSpec { + if in == nil { + return nil + } + out := new(PlatformAdminSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAdminStatus) DeepCopyInto(out *PlatformAdminStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]PlatformAdminCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAdminStatus. +func (in *PlatformAdminStatus) DeepCopy() *PlatformAdminStatus { + if in == nil { + return nil + } + out := new(PlatformAdminStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/apis/config/types.go b/pkg/controller/apis/config/types.go index 00232ae1ba0..58a86a245f2 100644 --- a/pkg/controller/apis/config/types.go +++ b/pkg/controller/apis/config/types.go @@ -20,6 +20,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" nodepoolconfig "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" + platformadminconfig "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" gatewayconfig "github.com/openyurtio/openyurt/pkg/controller/raven/config" yurtappdaemonconfig "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon/config" yurtappsetconfig "github.com/openyurtio/openyurt/pkg/controller/yurtappset/config" @@ -44,6 +45,9 @@ type YurtManagerConfiguration struct { // YurtAppDaemonControllerConfiguration holds configuration for YurtAppDaemonController related features. YurtAppDaemonController yurtappdaemonconfig.YurtAppDaemonControllerConfiguration + + // PlatformAdminControllerConfiguration holds configuration for PlatformAdminController related features. + PlatformAdminController platformadminconfig.PlatformAdminControllerConfiguration } type GenericConfiguration struct { diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 334197a1322..f4861eb9be9 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -25,6 +25,7 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/csrapprover" "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater" "github.com/openyurtio/openyurt/pkg/controller/nodepool" + "github.com/openyurtio/openyurt/pkg/controller/platformadmin" "github.com/openyurtio/openyurt/pkg/controller/raven" "github.com/openyurtio/openyurt/pkg/controller/raven/gateway" "github.com/openyurtio/openyurt/pkg/controller/raven/service" @@ -62,6 +63,7 @@ func init() { controllerAddFuncs[yurtstaticset.ControllerName] = []AddControllerFn{yurtstaticset.Add} controllerAddFuncs[yurtappset.ControllerName] = []AddControllerFn{yurtappset.Add} controllerAddFuncs[yurtappdaemon.ControllerName] = []AddControllerFn{yurtappdaemon.Add} + controllerAddFuncs[platformadmin.ControllerName] = []AddControllerFn{platformadmin.Add} } // If you want to add additional RBAC, enter it here !!! @kadisi diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json b/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json new file mode 100644 index 00000000000..eff97d58ac7 --- /dev/null +++ b/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json @@ -0,0 +1,4657 @@ +{ + "versions": [ + { + "versionName": "levski", + "configMaps": [ + { + "metadata": { + "name": "common-variable-levski", + "creationTimestamp": null + }, + "data": { + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "false", + "MESSAGEQUEUE_HOST": "edgex-redis", + "REGISTRY_HOST": "edgex-core-consul" + } + } + ], + "components": [ + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.3.0", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.3.0", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.3.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "openyurt/consul:1.13.2", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-data", + "image": "openyurt/core-data:2.3.0", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-command", + "image": "openyurt/core-command:2.3.0", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + }, + { + "name": "MESSAGEQUEUE_EXTERNAL_URL", + "value": "tcp://edgex-mqtt-broker:1883" + }, + { + "name": "MESSAGEQUEUE_INTERNAL_HOST", + "value": "edgex-redis" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-ui-go", + "service": { + "ports": [ + { + "name": "tcp-4000", + "protocol": "TCP", + "port": 4000, + "targetPort": 4000 + } + ], + "selector": { + "app": "edgex-ui-go" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-ui-go" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-ui-go" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:2.3.0", + "ports": [ + { + "name": "tcp-4000", + "containerPort": 4000, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-ui-go" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-ui-go" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.3.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.3.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.3.0", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.3.0", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "openyurt/redis:7.0.5-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "db-data", + "mountPath": "/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.7.1-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "jakarta", + "configMaps": [ + { + "metadata": { + "name": "common-variable-jakarta", + "creationTimestamp": null + }, + "data": { + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "false", + "MESSAGEQUEUE_HOST": "edgex-redis", + "REGISTRY_HOST": "edgex-core-consul" + } + } + ], + "components": [ + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.1.1", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.1.1", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.1.1", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.1.1", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.1.1", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.1.1", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "openyurt/redis:6.2.6-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "db-data", + "mountPath": "/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-command", + "image": "openyurt/core-command:2.1.1", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-data", + "image": "openyurt/core-data:2.1.1", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.1.1", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.4.4-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-ui-go", + "service": { + "ports": [ + { + "name": "tcp-4000", + "protocol": "TCP", + "port": 4000, + "targetPort": 4000 + } + ], + "selector": { + "app": "edgex-ui-go" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-ui-go" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-ui-go" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:2.1.0", + "ports": [ + { + "name": "tcp-4000", + "containerPort": 4000, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-ui-go" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "openyurt/consul:1.10.3", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "kamakura", + "configMaps": [ + { + "metadata": { + "name": "common-variable-kamakura", + "creationTimestamp": null + }, + "data": { + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "false", + "MESSAGEQUEUE_HOST": "edgex-redis", + "REGISTRY_HOST": "edgex-core-consul" + } + } + ], + "components": [ + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-command", + "image": "openyurt/core-command:2.2.0", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.2.0", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.2.0", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.2.0", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.2.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.4.4-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.2.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-ui-go", + "service": { + "ports": [ + { + "name": "tcp-4000", + "protocol": "TCP", + "port": 4000, + "targetPort": 4000 + } + ], + "selector": { + "app": "edgex-ui-go" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-ui-go" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-ui-go" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:2.2.0", + "ports": [ + { + "name": "tcp-4000", + "containerPort": 4000, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-ui-go" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-data", + "image": "openyurt/core-data:2.2.0", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.2.0", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "openyurt/redis:6.2.6-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "db-data", + "mountPath": "/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.2.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "openyurt/consul:1.10.10", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "ireland", + "configMaps": [ + { + "metadata": { + "name": "common-variable-ireland", + "creationTimestamp": null + }, + "data": { + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "false", + "MESSAGEQUEUE_HOST": "edgex-redis", + "REGISTRY_HOST": "edgex-core-consul" + } + } + ], + "components": [ + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-command", + "image": "openyurt/core-command:2.0.0", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.0.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.0.0", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.0.0", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "openyurt/consul:1.9.5", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.3.0-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "openyurt/redis:6.2.4-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "db-data", + "mountPath": "/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.0.1", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.0.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.0.0", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.0.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-data", + "image": "openyurt/core-data:2.0.0", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "hanoi", + "configMaps": [ + { + "metadata": { + "name": "common-variable-hanoi", + "creationTimestamp": null + }, + "data": { + "CLIENTS_COMMAND_HOST": "edgex-core-command", + "CLIENTS_COREDATA_HOST": "edgex-core-data", + "CLIENTS_DATA_HOST": "edgex-core-data", + "CLIENTS_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_RULESENGINE_HOST": "edgex-kuiper", + "CLIENTS_SCHEDULER_HOST": "edgex-support-scheduler", + "CLIENTS_VIRTUALDEVICE_HOST": "edgex-device-virtual", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "false", + "LOGGING_ENABLEREMOTE": "false", + "REGISTRY_HOST": "edgex-core-consul", + "SERVICE_SERVERBINDADDR": "0.0.0.0" + } + } + ], + "components": [ + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "openyurt/redis:6.0.9-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "db-data", + "mountPath": "/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-48085", + "protocol": "TCP", + "port": 48085, + "targetPort": 48085 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "openyurt/docker-support-scheduler-go:1.3.1", + "ports": [ + { + "name": "tcp-48085", + "containerPort": 48085, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "openyurt/docker-edgex-consul:1.3.0", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "EDGEX_DB", + "value": "redis" + }, + { + "name": "EDGEX_SECURE", + "value": "false" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-20498", + "protocol": "TCP", + "port": 20498, + "targetPort": 20498 + }, + { + "name": "tcp-48075", + "protocol": "TCP", + "port": 48075, + "targetPort": 48075 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-kuiper", + "image": "openyurt/kuiper:1.1.1-alpine", + "ports": [ + { + "name": "tcp-20498", + "containerPort": 20498, + "protocol": "TCP" + }, + { + "name": "tcp-48075", + "containerPort": 48075, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "48075" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "5566" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "tcp" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-app-service-configurable-rules" + }, + { + "name": "EDGEX__DEFAULT__SERVICESERVER", + "value": "http://edgex-core-data:48080" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "events" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-48082", + "protocol": "TCP", + "port": 48082, + "targetPort": 48082 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-command", + "image": "openyurt/docker-core-command-go:1.3.1", + "ports": [ + { + "name": "tcp-48082", + "containerPort": 48082, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-49990", + "protocol": "TCP", + "port": 49990, + "targetPort": 49990 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-virtual", + "image": "openyurt/docker-device-virtual-go:1.3.1", + "ports": [ + { + "name": "tcp-49990", + "containerPort": 49990, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-48060", + "protocol": "TCP", + "port": 48060, + "targetPort": 48060 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-notifications", + "image": "openyurt/docker-support-notifications-go:1.3.1", + "ports": [ + { + "name": "tcp-48060", + "containerPort": 48060, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-48081", + "protocol": "TCP", + "port": 48081, + "targetPort": 48081 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-metadata", + "image": "openyurt/docker-core-metadata-go:1.3.1", + "ports": [ + { + "name": "tcp-48081", + "containerPort": 48081, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-48080", + "protocol": "TCP", + "port": 48080, + "targetPort": 48080 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-data", + "image": "openyurt/docker-core-data-go:1.3.1", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-48080", + "containerPort": 48080, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-49986", + "protocol": "TCP", + "port": 49986, + "targetPort": 49986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "openyurt/docker-device-rest-go:1.2.1", + "ports": [ + { + "name": "tcp-49986", + "containerPort": 49986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-service-configurable-rules", + "service": { + "ports": [ + { + "name": "tcp-48100", + "protocol": "TCP", + "port": 48100, + "targetPort": 48100 + } + ], + "selector": { + "app": "edgex-app-service-configurable-rules" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-service-configurable-rules" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-service-configurable-rules" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-app-service-configurable-rules", + "image": "openyurt/docker-app-service-configurable:1.3.1", + "ports": [ + { + "name": "tcp-48100", + "containerPort": 48100, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "MESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_PORT", + "value": "48100" + }, + { + "name": "BINDING_PUBLISHTOPIC", + "value": "events" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-service-configurable-rules" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-service-configurable-rules" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-48090", + "protocol": "TCP", + "port": 48090, + "targetPort": 48090 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/docker-sys-mgmt-agent-go:1.3.1", + "ports": [ + { + "name": "tcp-48090", + "containerPort": 48090, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + } + ] + } + ] +} \ No newline at end of file diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/config.json b/pkg/controller/platformadmin/config/EdgeXConfig/config.json new file mode 100644 index 00000000000..cdd981dd785 --- /dev/null +++ b/pkg/controller/platformadmin/config/EdgeXConfig/config.json @@ -0,0 +1 @@ +{"versions":[{"versionName":"levski","configMaps":[{"metadata":{"name":"common-variable-levski","creationTimestamp":null},"data":{"API_GATEWAY_HOST":"edgex-kong","API_GATEWAY_STATUS_PORT":"8100","CLIENTS_CORE_COMMAND_HOST":"edgex-core-command","CLIENTS_CORE_DATA_HOST":"edgex-core-data","CLIENTS_CORE_METADATA_HOST":"edgex-core-metadata","CLIENTS_SUPPORT_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_SUPPORT_SCHEDULER_HOST":"edgex-support-scheduler","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","MESSAGEQUEUE_HOST":"edgex-redis","PROXY_SETUP_HOST":"edgex-security-proxy-setup","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_PORT":"8200","SPIFFE_ENDPOINTSOCKET":"/tmp/edgex/secrets/spiffe/public/api.sock","SPIFFE_TRUSTBUNDLE_PATH":"/tmp/edgex/secrets/spiffe/trust/bundle","SPIFFE_TRUSTDOMAIN":"edgexfoundry.org","STAGEGATE_BOOTSTRAPPER_HOST":"edgex-security-bootstrapper","STAGEGATE_BOOTSTRAPPER_STARTPORT":"54321","STAGEGATE_DATABASE_HOST":"edgex-redis","STAGEGATE_DATABASE_PORT":"6379","STAGEGATE_DATABASE_READYPORT":"6379","STAGEGATE_KONGDB_HOST":"edgex-kong-db","STAGEGATE_KONGDB_PORT":"5432","STAGEGATE_KONGDB_READYPORT":"54325","STAGEGATE_READY_TORUNPORT":"54329","STAGEGATE_REGISTRY_HOST":"edgex-core-consul","STAGEGATE_REGISTRY_PORT":"8500","STAGEGATE_REGISTRY_READYPORT":"54324","STAGEGATE_SECRETSTORESETUP_HOST":"edgex-security-secretstore-setup","STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT":"54322","STAGEGATE_WAITFOR_TIMEOUT":"60s"}}],"components":[{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-59882","protocol":"TCP","port":59882,"targetPort":59882}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/core-command:2.3.0","ports":[{"name":"tcp-59882","containerPort":59882,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"MESSAGEQUEUE_EXTERNAL_URL","value":"tcp://edgex-mqtt-broker:1883"},{"name":"SERVICE_HOST","value":"edgex-core-command"},{"name":"MESSAGEQUEUE_INTERNAL_HOST","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-app-rules-engine","service":{"ports":[{"name":"tcp-59701","protocol":"TCP","port":59701,"targetPort":59701}],"selector":{"app":"edgex-app-rules-engine"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-rules-engine"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-rules-engine"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/app-rules-engine","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-app-rules-engine","image":"openyurt/app-service-configurable:2.3.0","ports":[{"name":"tcp-59701","containerPort":59701,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"EDGEX_PROFILE","value":"rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST","value":"edgex-redis"},{"name":"SERVICE_HOST","value":"edgex-app-rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/app-rules-engine"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-rules-engine"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-59860","protocol":"TCP","port":59860,"targetPort":59860}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/support-notifications:2.3.0","ports":[{"name":"tcp-59860","containerPort":59860,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-59861","protocol":"TCP","port":59861,"targetPort":59861}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/support-scheduler:2.3.0","ports":[{"name":"tcp-59861","containerPort":59861,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-scheduler"},{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-59881","protocol":"TCP","port":59881,"targetPort":59881}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/core-metadata:2.3.0","ports":[{"name":"tcp-59881","containerPort":59881,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"},{"name":"SERVICE_HOST","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-59720","protocol":"TCP","port":59720,"targetPort":59720}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"kuiper-data","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}}],"containers":[{"name":"edgex-kuiper","image":"openyurt/ekuiper:1.7.1-alpine","ports":[{"name":"tcp-59720","containerPort":59720,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"EDGEX__DEFAULT__TYPE","value":"redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__SERVER","value":"edgex-redis"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"redis"},{"name":"KUIPER__BASIC__RESTPORT","value":"59720"},{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL","value":"redis"},{"name":"EDGEX__DEFAULT__TOPIC","value":"rules-events"},{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PORT","value":"6379"},{"name":"EDGEX__DEFAULT__PORT","value":"6379"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__TYPE","value":"redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"kuiper-data","mountPath":"/kuiper/data"},{"name":"kuiper-connections","mountPath":"/kuiper/etc/connections"},{"name":"kuiper-sources","mountPath":"/kuiper/etc/sources"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-security-secretstore-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-secretstore-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-secretstore-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"kong","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-security-secretstore-setup","image":"openyurt/security-secretstore-setup:2.3.0","envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"EDGEX_GROUP","value":"2001"},{"name":"SECUREMESSAGEBUS_TYPE","value":"redis"},{"name":"ADD_KNOWN_SECRETS","value":"redisdb[app-rules-engine],redisdb[device-rest],message-bus[device-rest],redisdb[device-virtual],message-bus[device-virtual]"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_SECRETSTORE_TOKENS"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"kong","mountPath":"/tmp/kong"},{"name":"kuiper-sources","mountPath":"/tmp/kuiper"},{"name":"kuiper-connections","mountPath":"/tmp/kuiper-connections"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-secretstore-setup"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-59900","protocol":"TCP","port":59900,"targetPort":59900}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-virtual","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-virtual","image":"openyurt/device-virtual:2.3.0","ports":[{"name":"tcp-59900","containerPort":59900,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-virtual"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-58890","protocol":"TCP","port":58890,"targetPort":58890}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/sys-mgmt-agent","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/sys-mgmt-agent:2.3.0","ports":[{"name":"tcp-58890","containerPort":58890,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"},{"name":"METRICSMECHANISM","value":"executor"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/sys-mgmt-agent"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/consul:1.13.2","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json"},{"name":"ADD_REGISTRY_ACL_ROLES"},{"name":"EDGEX_USER","value":"2002"},{"name":"STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH","value":"/consul/config/consul_acl_done"},{"name":"STAGEGATE_REGISTRY_ACL_MANAGEMENTTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/mgmt_token.json"},{"name":"EDGEX_GROUP","value":"2001"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-consul"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-ui-go","service":{"ports":[{"name":"tcp-4000","protocol":"TCP","port":4000,"targetPort":4000}],"selector":{"app":"edgex-ui-go"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-ui-go"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-ui-go"}},"spec":{"containers":[{"name":"edgex-ui-go","image":"openyurt/edgex-ui:2.3.0","ports":[{"name":"tcp-4000","containerPort":4000,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-ui-go"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-ui-go"}},"strategy":{}}},{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-59986","protocol":"TCP","port":59986,"targetPort":59986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-rest","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-rest","image":"openyurt/device-rest:2.3.0","ports":[{"name":"tcp-59986","containerPort":59986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-rest"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-security-bootstrapper","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrapper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrapper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}}],"containers":[{"name":"edgex-security-bootstrapper","image":"openyurt/security-bootstrapper:2.3.0","envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"EDGEX_USER","value":"2002"},{"name":"EDGEX_GROUP","value":"2001"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrapper"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-59880","protocol":"TCP","port":59880,"targetPort":59880}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/core-data:2.3.0","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-59880","containerPort":59880,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/core-data/secrets-token.json"},{"name":"SERVICE_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"edgex-kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8100","protocol":"TCP","port":8100,"targetPort":8100},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443}],"selector":{"app":"edgex-kong"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}},{"name":"postgres-config","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"edgex-kong","image":"openyurt/kong:2.8.1","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8100","containerPort":8100,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_ADMIN_LISTEN","value":"127.0.0.1:8001, 127.0.0.1:8444 ssl"},{"name":"KONG_PG_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_SSL_CIPHER_SUITE","value":"modern"},{"name":"KONG_STATUS_LISTEN","value":"0.0.0.0:8100"},{"name":"KONG_DNS_VALID_TTL","value":"1"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_DNS_ORDER","value":"LAST,A,CNAME"},{"name":"KONG_PG_HOST","value":"edgex-kong-db"},{"name":"KONG_NGINX_WORKER_PROCESSES","value":"1"},{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong"}},"strategy":{}}},{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"db-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"redis-config","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-bootstrapper-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:7.0.5-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"DATABASECONFIG_NAME","value":"redis.conf"},{"name":"DATABASECONFIG_PATH","value":"/run/redis/conf"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"db-data","mountPath":"/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"redis-config","mountPath":"/run/redis/conf"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-bootstrapper-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"vault-file","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.11.4","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"VAULT_UI","value":"true"},{"name":"VAULT_ADDR","value":"http://edgex-vault:8200"},{"name":"VAULT_CONFIG_DIR","value":"/vault/config"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-security-proxy-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-proxy-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-proxy-setup"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-proxy-setup","image":"openyurt/security-proxy-setup:2.3.0","envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"ROUTES_RULES_ENGINE_HOST","value":"edgex-kuiper"},{"name":"ROUTES_SYS_MGMT_AGENT_HOST","value":"edgex-sys-mgmt-agent"},{"name":"ROUTES_CORE_DATA_HOST","value":"edgex-core-data"},{"name":"ROUTES_CORE_COMMAND_HOST","value":"edgex-core-command"},{"name":"ADD_PROXY_ROUTE"},{"name":"ROUTES_SUPPORT_NOTIFICATIONS_HOST","value":"edgex-support-notifications"},{"name":"ROUTES_CORE_CONSUL_HOST","value":"edgex-core-consul"},{"name":"KONGURL_SERVER","value":"edgex-kong"},{"name":"ROUTES_CORE_METADATA_HOST","value":"edgex-core-metadata"},{"name":"ROUTES_DEVICE_VIRTUAL_HOST","value":"device-virtual"},{"name":"ROUTES_SUPPORT_SCHEDULER_HOST","value":"edgex-support-scheduler"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-proxy-setup"}},"strategy":{}}},{"name":"edgex-kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"edgex-kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"postgres-config","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"edgex-kong-db","image":"openyurt/postgres:13.8-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"POSTGRES_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"POSTGRES_USER","value":"kong"},{"name":"POSTGRES_DB","value":"kong"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong-db"}},"strategy":{}}}]},{"versionName":"jakarta","configMaps":[{"metadata":{"name":"common-variable-jakarta","creationTimestamp":null},"data":{"API_GATEWAY_HOST":"edgex-kong","API_GATEWAY_STATUS_PORT":"8100","CLIENTS_CORE_COMMAND_HOST":"edgex-core-command","CLIENTS_CORE_DATA_HOST":"edgex-core-data","CLIENTS_CORE_METADATA_HOST":"edgex-core-metadata","CLIENTS_SUPPORT_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_SUPPORT_SCHEDULER_HOST":"edgex-support-scheduler","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","MESSAGEQUEUE_HOST":"edgex-redis","PROXY_SETUP_HOST":"edgex-security-proxy-setup","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_PORT":"8200","STAGEGATE_BOOTSTRAPPER_HOST":"edgex-security-bootstrapper","STAGEGATE_BOOTSTRAPPER_STARTPORT":"54321","STAGEGATE_DATABASE_HOST":"edgex-redis","STAGEGATE_DATABASE_PORT":"6379","STAGEGATE_DATABASE_READYPORT":"6379","STAGEGATE_KONGDB_HOST":"edgex-kong-db","STAGEGATE_KONGDB_PORT":"5432","STAGEGATE_KONGDB_READYPORT":"54325","STAGEGATE_READY_TORUNPORT":"54329","STAGEGATE_REGISTRY_HOST":"edgex-core-consul","STAGEGATE_REGISTRY_PORT":"8500","STAGEGATE_REGISTRY_READYPORT":"54324","STAGEGATE_SECRETSTORESETUP_HOST":"edgex-security-secretstore-setup","STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT":"54322","STAGEGATE_WAITFOR_TIMEOUT":"60s"}}],"components":[{"name":"edgex-app-rules-engine","service":{"ports":[{"name":"tcp-59701","protocol":"TCP","port":59701,"targetPort":59701}],"selector":{"app":"edgex-app-rules-engine"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-rules-engine"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-rules-engine"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/app-rules-engine","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-app-rules-engine","image":"openyurt/app-service-configurable:2.1.1","ports":[{"name":"tcp-59701","containerPort":59701,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-app-rules-engine"},{"name":"EDGEX_PROFILE","value":"rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST","value":"edgex-redis"},{"name":"TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/app-rules-engine"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-rules-engine"}},"strategy":{}}},{"name":"edgex-ui-go","service":{"ports":[{"name":"tcp-4000","protocol":"TCP","port":4000,"targetPort":4000}],"selector":{"app":"edgex-ui-go"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-ui-go"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-ui-go"}},"spec":{"containers":[{"name":"edgex-ui-go","image":"openyurt/edgex-ui:2.1.0","ports":[{"name":"tcp-4000","containerPort":4000,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-ui-go"}},"strategy":{}}},{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"db-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"redis-config","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-bootstrapper-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:6.2.6-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"DATABASECONFIG_PATH","value":"/run/redis/conf"},{"name":"DATABASECONFIG_NAME","value":"redis.conf"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"db-data","mountPath":"/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"redis-config","mountPath":"/run/redis/conf"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-bootstrapper-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-58890","protocol":"TCP","port":58890,"targetPort":58890}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/sys-mgmt-agent","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/sys-mgmt-agent:2.1.1","ports":[{"name":"tcp-58890","containerPort":58890,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"METRICSMECHANISM","value":"executor"},{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/sys-mgmt-agent"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-59880","protocol":"TCP","port":59880,"targetPort":59880}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/core-data:2.1.1","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-59880","containerPort":59880,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-data"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/core-data/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-59720","protocol":"TCP","port":59720,"targetPort":59720}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"kuiper-data","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}}],"containers":[{"name":"edgex-kuiper","image":"openyurt/ekuiper:1.4.4-alpine","ports":[{"name":"tcp-59720","containerPort":59720,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"EDGEX__DEFAULT__TYPE","value":"redis"},{"name":"EDGEX__DEFAULT__PORT","value":"6379"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL","value":"redis"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"redis"},{"name":"EDGEX__DEFAULT__TOPIC","value":"rules-events"},{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__SERVER","value":"edgex-redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PORT","value":"6379"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__TYPE","value":"redis"},{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"KUIPER__BASIC__RESTPORT","value":"59720"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"kuiper-data","mountPath":"/kuiper/data"},{"name":"kuiper-connections","mountPath":"/kuiper/etc/connections"},{"name":"kuiper-sources","mountPath":"/kuiper/etc/sources"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-59900","protocol":"TCP","port":59900,"targetPort":59900}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-virtual","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-virtual","image":"openyurt/device-virtual:2.1.1","ports":[{"name":"tcp-59900","containerPort":59900,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-virtual"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-59861","protocol":"TCP","port":59861,"targetPort":59861}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/support-scheduler:2.1.1","ports":[{"name":"tcp-59861","containerPort":59861,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"},{"name":"SERVICE_HOST","value":"edgex-support-scheduler"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-59882","protocol":"TCP","port":59882,"targetPort":59882}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/core-command:2.1.1","ports":[{"name":"tcp-59882","containerPort":59882,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-command"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-59881","protocol":"TCP","port":59881,"targetPort":59881}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/core-metadata:2.1.1","ports":[{"name":"tcp-59881","containerPort":59881,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-metadata"},{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}},{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-59986","protocol":"TCP","port":59986,"targetPort":59986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-rest","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-rest","image":"openyurt/device-rest:2.1.1","ports":[{"name":"tcp-59986","containerPort":59986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-rest"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-security-bootstrapper","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrapper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrapper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}}],"containers":[{"name":"edgex-security-bootstrapper","image":"openyurt/security-bootstrapper:2.1.1","envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"EDGEX_GROUP","value":"2001"},{"name":"EDGEX_USER","value":"2002"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrapper"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-59860","protocol":"TCP","port":59860,"targetPort":59860}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/support-notifications:2.1.1","ports":[{"name":"tcp-59860","containerPort":59860,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"vault-file","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.8.4","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"VAULT_CONFIG_DIR","value":"/vault/config"},{"name":"VAULT_ADDR","value":"http://edgex-vault:8200"},{"name":"VAULT_UI","value":"true"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8100","protocol":"TCP","port":8100,"targetPort":8100},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443}],"selector":{"app":"edgex-kong"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}},{"name":"postgres-config","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"edgex-kong","image":"openyurt/kong:2.5.1","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8100","containerPort":8100,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"KONG_DNS_VALID_TTL","value":"1"},{"name":"KONG_SSL_CIPHER_SUITE","value":"modern"},{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_ADMIN_LISTEN","value":"127.0.0.1:8001, 127.0.0.1:8444 ssl"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_NGINX_WORKER_PROCESSES","value":"1"},{"name":"KONG_DNS_ORDER","value":"LAST,A,CNAME"},{"name":"KONG_PG_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_STATUS_LISTEN","value":"0.0.0.0:8100"},{"name":"KONG_PG_HOST","value":"edgex-kong-db"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong"}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/consul:1.10.3","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json"},{"name":"EDGEX_GROUP","value":"2001"},{"name":"STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH","value":"/consul/config/consul_acl_done"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_REGISTRY_ACL_ROLES"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-consul"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-security-proxy-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-proxy-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-proxy-setup"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-proxy-setup","image":"openyurt/security-proxy-setup:2.1.1","envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"KONGURL_SERVER","value":"edgex-kong"},{"name":"ADD_PROXY_ROUTE"},{"name":"ROUTES_CORE_DATA_HOST","value":"edgex-core-data"},{"name":"ROUTES_RULES_ENGINE_HOST","value":"edgex-kuiper"},{"name":"ROUTES_SUPPORT_NOTIFICATIONS_HOST","value":"edgex-support-notifications"},{"name":"ROUTES_CORE_CONSUL_HOST","value":"edgex-core-consul"},{"name":"ROUTES_SYS_MGMT_AGENT_HOST","value":"edgex-sys-mgmt-agent"},{"name":"ROUTES_DEVICE_VIRTUAL_HOST","value":"device-virtual"},{"name":"ROUTES_SUPPORT_SCHEDULER_HOST","value":"edgex-support-scheduler"},{"name":"ROUTES_CORE_COMMAND_HOST","value":"edgex-core-command"},{"name":"ROUTES_CORE_METADATA_HOST","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-proxy-setup"}},"strategy":{}}},{"name":"edgex-security-secretstore-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-secretstore-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-secretstore-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"kong","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-security-secretstore-setup","image":"openyurt/security-secretstore-setup:2.1.1","envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"EDGEX_GROUP","value":"2001"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_KNOWN_SECRETS","value":"redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]"},{"name":"ADD_SECRETSTORE_TOKENS"},{"name":"SECUREMESSAGEBUS_TYPE","value":"redis"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"kong","mountPath":"/tmp/kong"},{"name":"kuiper-sources","mountPath":"/tmp/kuiper"},{"name":"kuiper-connections","mountPath":"/tmp/kuiper-connections"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-secretstore-setup"}},"strategy":{}}},{"name":"edgex-kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"edgex-kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"postgres-config","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"edgex-kong-db","image":"openyurt/postgres:13.4-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"POSTGRES_USER","value":"kong"},{"name":"POSTGRES_DB","value":"kong"},{"name":"POSTGRES_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong-db"}},"strategy":{}}}]},{"versionName":"kamakura","configMaps":[{"metadata":{"name":"common-variable-kamakura","creationTimestamp":null},"data":{"API_GATEWAY_HOST":"edgex-kong","API_GATEWAY_STATUS_PORT":"8100","CLIENTS_CORE_COMMAND_HOST":"edgex-core-command","CLIENTS_CORE_DATA_HOST":"edgex-core-data","CLIENTS_CORE_METADATA_HOST":"edgex-core-metadata","CLIENTS_SUPPORT_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_SUPPORT_SCHEDULER_HOST":"edgex-support-scheduler","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","MESSAGEQUEUE_HOST":"edgex-redis","PROXY_SETUP_HOST":"edgex-security-proxy-setup","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_PORT":"8200","SPIFFE_ENDPOINTSOCKET":"/tmp/edgex/secrets/spiffe/public/api.sock","SPIFFE_TRUSTBUNDLE_PATH":"/tmp/edgex/secrets/spiffe/trust/bundle","SPIFFE_TRUSTDOMAIN":"edgexfoundry.org","STAGEGATE_BOOTSTRAPPER_HOST":"edgex-security-bootstrapper","STAGEGATE_BOOTSTRAPPER_STARTPORT":"54321","STAGEGATE_DATABASE_HOST":"edgex-redis","STAGEGATE_DATABASE_PORT":"6379","STAGEGATE_DATABASE_READYPORT":"6379","STAGEGATE_KONGDB_HOST":"edgex-kong-db","STAGEGATE_KONGDB_PORT":"5432","STAGEGATE_KONGDB_READYPORT":"54325","STAGEGATE_READY_TORUNPORT":"54329","STAGEGATE_REGISTRY_HOST":"edgex-core-consul","STAGEGATE_REGISTRY_PORT":"8500","STAGEGATE_REGISTRY_READYPORT":"54324","STAGEGATE_SECRETSTORESETUP_HOST":"edgex-security-secretstore-setup","STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT":"54322","STAGEGATE_WAITFOR_TIMEOUT":"60s"}}],"components":[{"name":"edgex-security-bootstrapper","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrapper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrapper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}}],"containers":[{"name":"edgex-security-bootstrapper","image":"openyurt/security-bootstrapper:2.2.0","envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"EDGEX_GROUP","value":"2001"},{"name":"EDGEX_USER","value":"2002"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrapper"}},"strategy":{}}},{"name":"edgex-app-rules-engine","service":{"ports":[{"name":"tcp-59701","protocol":"TCP","port":59701,"targetPort":59701}],"selector":{"app":"edgex-app-rules-engine"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-rules-engine"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-rules-engine"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/app-rules-engine","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-app-rules-engine","image":"openyurt/app-service-configurable:2.2.0","ports":[{"name":"tcp-59701","containerPort":59701,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-redis"},{"name":"TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST","value":"edgex-redis"},{"name":"EDGEX_PROFILE","value":"rules-engine"},{"name":"SERVICE_HOST","value":"edgex-app-rules-engine"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/app-rules-engine"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-rules-engine"}},"strategy":{}}},{"name":"edgex-security-proxy-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-proxy-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-proxy-setup"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-proxy-setup","image":"openyurt/security-proxy-setup:2.2.0","envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"ROUTES_RULES_ENGINE_HOST","value":"edgex-kuiper"},{"name":"KONGURL_SERVER","value":"edgex-kong"},{"name":"ROUTES_DEVICE_VIRTUAL_HOST","value":"device-virtual"},{"name":"ROUTES_CORE_DATA_HOST","value":"edgex-core-data"},{"name":"ROUTES_CORE_COMMAND_HOST","value":"edgex-core-command"},{"name":"ADD_PROXY_ROUTE"},{"name":"ROUTES_SYS_MGMT_AGENT_HOST","value":"edgex-sys-mgmt-agent"},{"name":"ROUTES_SUPPORT_NOTIFICATIONS_HOST","value":"edgex-support-notifications"},{"name":"ROUTES_CORE_METADATA_HOST","value":"edgex-core-metadata"},{"name":"ROUTES_CORE_CONSUL_HOST","value":"edgex-core-consul"},{"name":"ROUTES_SUPPORT_SCHEDULER_HOST","value":"edgex-support-scheduler"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-proxy-setup"}},"strategy":{}}},{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-59986","protocol":"TCP","port":59986,"targetPort":59986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-rest","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-rest","image":"openyurt/device-rest:2.2.0","ports":[{"name":"tcp-59986","containerPort":59986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-rest"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-59720","protocol":"TCP","port":59720,"targetPort":59720}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"kuiper-data","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}}],"containers":[{"name":"edgex-kuiper","image":"openyurt/ekuiper:1.4.4-alpine","ports":[{"name":"tcp-59720","containerPort":59720,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"EDGEX__DEFAULT__TOPIC","value":"rules-events"},{"name":"EDGEX__DEFAULT__PORT","value":"6379"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PORT","value":"6379"},{"name":"KUIPER__BASIC__RESTPORT","value":"59720"},{"name":"EDGEX__DEFAULT__TYPE","value":"redis"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL","value":"redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__SERVER","value":"edgex-redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__TYPE","value":"redis"},{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"kuiper-data","mountPath":"/kuiper/data"},{"name":"kuiper-connections","mountPath":"/kuiper/etc/connections"},{"name":"kuiper-sources","mountPath":"/kuiper/etc/sources"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-58890","protocol":"TCP","port":58890,"targetPort":58890}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/sys-mgmt-agent","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/sys-mgmt-agent:2.2.0","ports":[{"name":"tcp-58890","containerPort":58890,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"},{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"METRICSMECHANISM","value":"executor"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/sys-mgmt-agent"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-59881","protocol":"TCP","port":59881,"targetPort":59881}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/core-metadata:2.2.0","ports":[{"name":"tcp-59881","containerPort":59881,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-metadata"},{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-59880","protocol":"TCP","port":59880,"targetPort":59880}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/core-data:2.2.0","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-59880","containerPort":59880,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/core-data/secrets-token.json"},{"name":"SERVICE_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"edgex-security-secretstore-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-secretstore-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-secretstore-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"kong","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-security-secretstore-setup","image":"openyurt/security-secretstore-setup:2.2.0","envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"ADD_SECRETSTORE_TOKENS"},{"name":"EDGEX_GROUP","value":"2001"},{"name":"SECUREMESSAGEBUS_TYPE","value":"redis"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_KNOWN_SECRETS","value":"redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"kong","mountPath":"/tmp/kong"},{"name":"kuiper-sources","mountPath":"/tmp/kuiper"},{"name":"kuiper-connections","mountPath":"/tmp/kuiper-connections"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-secretstore-setup"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-59900","protocol":"TCP","port":59900,"targetPort":59900}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-virtual","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-virtual","image":"openyurt/device-virtual:2.2.0","ports":[{"name":"tcp-59900","containerPort":59900,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-virtual"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"edgex-ui-go","service":{"ports":[{"name":"tcp-4000","protocol":"TCP","port":4000,"targetPort":4000}],"selector":{"app":"edgex-ui-go"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-ui-go"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-ui-go"}},"spec":{"containers":[{"name":"edgex-ui-go","image":"openyurt/edgex-ui:2.2.0","ports":[{"name":"tcp-4000","containerPort":4000,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-ui-go"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"vault-file","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.8.9","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"VAULT_CONFIG_DIR","value":"/vault/config"},{"name":"VAULT_ADDR","value":"http://edgex-vault:8200"},{"name":"VAULT_UI","value":"true"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-59882","protocol":"TCP","port":59882,"targetPort":59882}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/core-command:2.2.0","ports":[{"name":"tcp-59882","containerPort":59882,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-command"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/consul:1.10.10","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"ADD_REGISTRY_ACL_ROLES"},{"name":"STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH","value":"/consul/config/consul_acl_done"},{"name":"STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json"},{"name":"EDGEX_USER","value":"2002"},{"name":"EDGEX_GROUP","value":"2001"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-consul"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"edgex-kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"postgres-config","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"edgex-kong-db","image":"openyurt/postgres:13.5-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"POSTGRES_USER","value":"kong"},{"name":"POSTGRES_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"POSTGRES_DB","value":"kong"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong-db"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-59861","protocol":"TCP","port":59861,"targetPort":59861}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/support-scheduler:2.2.0","ports":[{"name":"tcp-59861","containerPort":59861,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"SERVICE_HOST","value":"edgex-support-scheduler"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"db-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"redis-config","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-bootstrapper-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:6.2.6-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"DATABASECONFIG_NAME","value":"redis.conf"},{"name":"DATABASECONFIG_PATH","value":"/run/redis/conf"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"db-data","mountPath":"/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"redis-config","mountPath":"/run/redis/conf"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-bootstrapper-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-59860","protocol":"TCP","port":59860,"targetPort":59860}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/support-notifications:2.2.0","ports":[{"name":"tcp-59860","containerPort":59860,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}},{"name":"edgex-kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8100","protocol":"TCP","port":8100,"targetPort":8100},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443}],"selector":{"app":"edgex-kong"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}},{"name":"postgres-config","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"edgex-kong","image":"openyurt/kong:2.6.1","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8100","containerPort":8100,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"KONG_NGINX_WORKER_PROCESSES","value":"1"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_PG_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"KONG_STATUS_LISTEN","value":"0.0.0.0:8100"},{"name":"KONG_SSL_CIPHER_SUITE","value":"modern"},{"name":"KONG_DNS_ORDER","value":"LAST,A,CNAME"},{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_DNS_VALID_TTL","value":"1"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_PG_HOST","value":"edgex-kong-db"},{"name":"KONG_ADMIN_LISTEN","value":"127.0.0.1:8001, 127.0.0.1:8444 ssl"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong"}},"strategy":{}}}]},{"versionName":"ireland","configMaps":[{"metadata":{"name":"common-variable-ireland","creationTimestamp":null},"data":{"API_GATEWAY_HOST":"edgex-kong","API_GATEWAY_STATUS_PORT":"8100","CLIENTS_CORE_COMMAND_HOST":"edgex-core-command","CLIENTS_CORE_DATA_HOST":"edgex-core-data","CLIENTS_CORE_METADATA_HOST":"edgex-core-metadata","CLIENTS_SUPPORT_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_SUPPORT_SCHEDULER_HOST":"edgex-support-scheduler","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","MESSAGEQUEUE_HOST":"edgex-redis","PROXY_SETUP_HOST":"edgex-security-proxy-setup","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_PORT":"8200","STAGEGATE_BOOTSTRAPPER_HOST":"edgex-security-bootstrapper","STAGEGATE_BOOTSTRAPPER_STARTPORT":"54321","STAGEGATE_DATABASE_HOST":"edgex-redis","STAGEGATE_DATABASE_PORT":"6379","STAGEGATE_DATABASE_READYPORT":"6379","STAGEGATE_KONGDB_HOST":"edgex-kong-db","STAGEGATE_KONGDB_PORT":"5432","STAGEGATE_KONGDB_READYPORT":"54325","STAGEGATE_READY_TORUNPORT":"54329","STAGEGATE_REGISTRY_HOST":"edgex-core-consul","STAGEGATE_REGISTRY_PORT":"8500","STAGEGATE_REGISTRY_READYPORT":"54324","STAGEGATE_SECRETSTORESETUP_HOST":"edgex-security-secretstore-setup","STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT":"54322","STAGEGATE_WAITFOR_TIMEOUT":"60s"}}],"components":[{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"db-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"redis-config","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-bootstrapper-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:6.2.4-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"DATABASECONFIG_NAME","value":"redis.conf"},{"name":"DATABASECONFIG_PATH","value":"/run/redis/conf"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"db-data","mountPath":"/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"redis-config","mountPath":"/run/redis/conf"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-bootstrapper-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-59900","protocol":"TCP","port":59900,"targetPort":59900}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-virtual","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-virtual","image":"openyurt/device-virtual:2.0.0","ports":[{"name":"tcp-59900","containerPort":59900,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-virtual"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-59881","protocol":"TCP","port":59881,"targetPort":59881}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/core-metadata:2.0.0","ports":[{"name":"tcp-59881","containerPort":59881,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-metadata"},{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}},{"name":"edgex-app-rules-engine","service":{"ports":[{"name":"tcp-59701","protocol":"TCP","port":59701,"targetPort":59701}],"selector":{"app":"edgex-app-rules-engine"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-rules-engine"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-rules-engine"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/app-rules-engine","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-app-rules-engine","image":"openyurt/app-service-configurable:2.0.1","ports":[{"name":"tcp-59701","containerPort":59701,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"EDGEX_PROFILE","value":"rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-redis"},{"name":"SERVICE_HOST","value":"edgex-app-rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/app-rules-engine"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-rules-engine"}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/consul:1.9.5","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json"},{"name":"EDGEX_GROUP","value":"2001"},{"name":"ADD_REGISTRY_ACL_ROLES"},{"name":"STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH","value":"/consul/config/consul_acl_done"},{"name":"EDGEX_USER","value":"2002"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-consul"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-59986","protocol":"TCP","port":59986,"targetPort":59986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-rest","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-rest","image":"openyurt/device-rest:2.0.0","ports":[{"name":"tcp-59986","containerPort":59986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-rest"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"vault-file","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.7.2","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"VAULT_ADDR","value":"http://edgex-vault:8200"},{"name":"VAULT_UI","value":"true"},{"name":"VAULT_CONFIG_DIR","value":"/vault/config"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-59882","protocol":"TCP","port":59882,"targetPort":59882}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/core-command:2.0.0","ports":[{"name":"tcp-59882","containerPort":59882,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-command"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-59880","protocol":"TCP","port":59880,"targetPort":59880}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/core-data:2.0.0","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-59880","containerPort":59880,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-data"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/core-data/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-59720","protocol":"TCP","port":59720,"targetPort":59720}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"kuiper-data","emptyDir":{}},{"name":"kuiper-config","emptyDir":{}}],"containers":[{"name":"edgex-kuiper","image":"openyurt/ekuiper:1.3.0-alpine","ports":[{"name":"tcp-59720","containerPort":59720,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-redis"},{"name":"EDGEX__DEFAULT__TYPE","value":"redis"},{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"EDGEX__DEFAULT__TOPIC","value":"rules-events"},{"name":"EDGEX__DEFAULT__PORT","value":"6379"},{"name":"KUIPER__BASIC__RESTPORT","value":"59720"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"kuiper-data","mountPath":"/kuiper/data"},{"name":"kuiper-config","mountPath":"/kuiper/etc/sources"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-security-proxy-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-proxy-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-proxy-setup"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-proxy-setup","image":"openyurt/security-proxy-setup:2.0.0","envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"ROUTES_SYS_MGMT_AGENT_HOST","value":"edgex-sys-mgmt-agent"},{"name":"ROUTES_CORE_METADATA_HOST","value":"edgex-core-metadata"},{"name":"ROUTES_DEVICE_VIRTUAL_HOST","value":"device-virtual"},{"name":"ROUTES_RULES_ENGINE_HOST","value":"edgex-kuiper"},{"name":"ROUTES_SUPPORT_SCHEDULER_HOST","value":"edgex-support-scheduler"},{"name":"KONGURL_SERVER","value":"edgex-kong"},{"name":"ROUTES_CORE_DATA_HOST","value":"edgex-core-data"},{"name":"ROUTES_SUPPORT_NOTIFICATIONS_HOST","value":"edgex-support-notifications"},{"name":"ADD_PROXY_ROUTE"},{"name":"ROUTES_CORE_COMMAND_HOST","value":"edgex-core-command"},{"name":"ROUTES_CORE_CONSUL_HOST","value":"edgex-core-consul"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-proxy-setup"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-59861","protocol":"TCP","port":59861,"targetPort":59861}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/support-scheduler:2.0.0","ports":[{"name":"tcp-59861","containerPort":59861,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"SERVICE_HOST","value":"edgex-support-scheduler"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8100","protocol":"TCP","port":8100,"targetPort":8100},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443}],"selector":{"app":"edgex-kong"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}},{"name":"postgres-config","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"edgex-kong","image":"openyurt/kong:2.4.1-alpine","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8100","containerPort":8100,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_DNS_VALID_TTL","value":"1"},{"name":"KONG_STATUS_LISTEN","value":"0.0.0.0:8100"},{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_DNS_ORDER","value":"LAST,A,CNAME"},{"name":"KONG_PG_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"KONG_PG_HOST","value":"edgex-kong-db"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_ADMIN_LISTEN","value":"127.0.0.1:8001, 127.0.0.1:8444 ssl"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong"}},"strategy":{}}},{"name":"edgex-security-secretstore-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-secretstore-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-secretstore-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"kong","emptyDir":{}},{"name":"kuiper-config","emptyDir":{}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-security-secretstore-setup","image":"openyurt/security-secretstore-setup:2.0.0","envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"ADD_KNOWN_SECRETS","value":"redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]"},{"name":"EDGEX_GROUP","value":"2001"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_SECRETSTORE_TOKENS"},{"name":"SECUREMESSAGEBUS_TYPE","value":"redis"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"kong","mountPath":"/tmp/kong"},{"name":"kuiper-config","mountPath":"/tmp/kuiper"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-secretstore-setup"}},"strategy":{}}},{"name":"edgex-kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"edgex-kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"postgres-config","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"edgex-kong-db","image":"openyurt/postgres:12.3-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"POSTGRES_DB","value":"kong"},{"name":"POSTGRES_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"POSTGRES_USER","value":"kong"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong-db"}},"strategy":{}}},{"name":"edgex-security-bootstrapper","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrapper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrapper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}}],"containers":[{"name":"edgex-security-bootstrapper","image":"openyurt/security-bootstrapper:2.0.0","envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"EDGEX_USER","value":"2002"},{"name":"EDGEX_GROUP","value":"2001"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrapper"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-58890","protocol":"TCP","port":58890,"targetPort":58890}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/sys-mgmt-agent","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/sys-mgmt-agent:2.0.0","ports":[{"name":"tcp-58890","containerPort":58890,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"},{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"METRICSMECHANISM","value":"executor"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/sys-mgmt-agent"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-59860","protocol":"TCP","port":59860,"targetPort":59860}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/support-notifications:2.0.0","ports":[{"name":"tcp-59860","containerPort":59860,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}}]},{"versionName":"hanoi","configMaps":[{"metadata":{"name":"common-variable-hanoi","creationTimestamp":null},"data":{"CLIENTS_COMMAND_HOST":"edgex-core-command","CLIENTS_COREDATA_HOST":"edgex-core-data","CLIENTS_DATA_HOST":"edgex-core-data","CLIENTS_METADATA_HOST":"edgex-core-metadata","CLIENTS_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_RULESENGINE_HOST":"edgex-kuiper","CLIENTS_SCHEDULER_HOST":"edgex-support-scheduler","CLIENTS_VIRTUALDEVICE_HOST":"edgex-device-virtual","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","LOGGING_ENABLEREMOTE":"false","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_ROOTCACERTPATH":"/tmp/edgex/secrets/ca/ca.pem","SECRETSTORE_SERVERNAME":"edgex-vault","SERVICE_SERVERBINDADDR":"0.0.0.0"}}],"components":[{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-49986","protocol":"TCP","port":49986,"targetPort":49986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"containers":[{"name":"edgex-device-rest","image":"openyurt/docker-device-rest-go:1.2.1","ports":[{"name":"tcp-49986","containerPort":49986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-48082","protocol":"TCP","port":48082,"targetPort":48082}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/docker-core-command-go:1.3.1","ports":[{"name":"tcp-48082","containerPort":48082,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-command"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-core-command/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-48060","protocol":"TCP","port":48060,"targetPort":48060}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/docker-support-notifications-go:1.3.1","ports":[{"name":"tcp-48060","containerPort":48060,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-support-notifications/secrets-token.json"},{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}},{"name":"edgex-vault-worker","deployment":{"selector":{"matchLabels":{"app":"edgex-vault-worker"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault-worker"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"consul-scripts","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-vault-worker","image":"openyurt/docker-security-secretstore-setup-go:1.3.1","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSTORE_SETUP_DONE_FLAG","value":"/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"consul-scripts","mountPath":"/consul/scripts"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault-worker"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-48085","protocol":"TCP","port":48085,"targetPort":48085}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/docker-support-scheduler-go:1.3.1","ports":[{"name":"tcp-48085","containerPort":48085,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-support-scheduler/secrets-token.json"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"},{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"SERVICE_HOST","value":"edgex-support-scheduler"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-secrets-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-secrets-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-secrets-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"secrets-setup-cache","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"vault-init","emptyDir":{}}],"containers":[{"name":"edgex-secrets-setup","image":"openyurt/docker-security-secrets-setup-go:1.3.1","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/tmp"},{"name":"tmpfs-volume2","mountPath":"/run"},{"name":"secrets-setup-cache","mountPath":"/etc/edgex/pki"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"vault-init","mountPath":"/vault/init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-secrets-setup"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-vault","type":"DirectoryOrCreate"}},{"name":"vault-file","emptyDir":{}},{"name":"vault-init","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.5.3","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"VAULT_CONFIG_DIR","value":"/vault/config"},{"name":"VAULT_UI","value":"true"},{"name":"VAULT_ADDR","value":"https://edgex-vault:8200"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-vault"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-init","mountPath":"/vault/init"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-48080","protocol":"TCP","port":48080,"targetPort":48080}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/docker-core-data-go:1.3.1","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-48080","containerPort":48080,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-data"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-core-data/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8001","protocol":"TCP","port":8001,"targetPort":8001},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443},{"name":"tcp-8444","protocol":"TCP","port":8444,"targetPort":8444}],"selector":{"app":"kong"}},"deployment":{"selector":{"matchLabels":{"app":"kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"consul-scripts","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"kong","image":"openyurt/kong:2.0.5","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8001","containerPort":8001,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"},{"name":"tcp-8444","containerPort":8444,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_ADMIN_LISTEN","value":"0.0.0.0:8001, 0.0.0.0:8444 ssl"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_PG_HOST","value":"kong-db"},{"name":"KONG_PG_PASSWORD","value":"kong"},{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"consul-scripts","mountPath":"/consul/scripts"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"kong"}},"strategy":{}}},{"name":"","deployment":{"selector":{"matchLabels":{"app":""}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":""}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"consul-scripts","emptyDir":{}}],"containers":[{"name":"","image":"openyurt/kong:2.0.5","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"KONG_PG_PASSWORD","value":"kong"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_PG_HOST","value":"kong-db"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/tmp"},{"name":"consul-scripts","mountPath":"/consul/scripts"}],"imagePullPolicy":"IfNotPresent"}]}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"consul-scripts","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}},{"name":"anonymous-volume3","hostPath":{"path":"/tmp/edgex/secrets/edgex-kong","type":"DirectoryOrCreate"}},{"name":"anonymous-volume4","hostPath":{"path":"/tmp/edgex/secrets/edgex-vault","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/docker-edgex-consul:1.3.0","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSTORE_SETUP_DONE_FLAG","value":"/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done"},{"name":"EDGEX_DB","value":"redis"},{"name":"EDGEX_SECURE","value":"true"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"consul-scripts","mountPath":"/consul/scripts"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-consul"},{"name":"anonymous-volume3","mountPath":"/tmp/edgex/secrets/edgex-kong"},{"name":"anonymous-volume4","mountPath":"/tmp/edgex/secrets/edgex-vault"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-security-bootstrap-database","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrap-database"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrap-database"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-security-bootstrap-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-bootstrap-database","image":"openyurt/docker-security-bootstrap-redis-go:1.3.1","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-security-bootstrap-database"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-security-bootstrap-redis/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-security-bootstrap-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrap-database"}},"strategy":{}}},{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"db-data","emptyDir":{}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:6.0.9-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"resources":{},"volumeMounts":[{"name":"db-data","mountPath":"/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-49990","protocol":"TCP","port":49990,"targetPort":49990}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"containers":[{"name":"edgex-device-virtual","image":"openyurt/docker-device-virtual-go:1.3.1","ports":[{"name":"tcp-49990","containerPort":49990,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"kong-db","image":"openyurt/postgres:12.3-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"POSTGRES_DB","value":"kong"},{"name":"POSTGRES_PASSWORD","value":"kong"},{"name":"POSTGRES_USER","value":"kong"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"kong-db"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-48090","protocol":"TCP","port":48090,"targetPort":48090}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/docker-sys-mgmt-agent-go:1.3.1","ports":[{"name":"tcp-48090","containerPort":48090,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"},{"name":"METRICSMECHANISM","value":"executor"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-20498","protocol":"TCP","port":20498,"targetPort":20498},{"name":"tcp-48075","protocol":"TCP","port":48075,"targetPort":48075}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"containers":[{"name":"edgex-kuiper","image":"openyurt/kuiper:1.1.1-alpine","ports":[{"name":"tcp-20498","containerPort":20498,"protocol":"TCP"},{"name":"tcp-48075","containerPort":48075,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"EDGEX__DEFAULT__SERVICESERVER","value":"http://edgex-core-data:48080"},{"name":"EDGEX__DEFAULT__TOPIC","value":"events"},{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"KUIPER__BASIC__RESTPORT","value":"48075"},{"name":"EDGEX__DEFAULT__PORT","value":"5566"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"tcp"},{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-app-service-configurable-rules"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-proxy","deployment":{"selector":{"matchLabels":{"app":"edgex-proxy"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-proxy"}},"spec":{"volumes":[{"name":"consul-scripts","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-proxy","image":"openyurt/docker-security-proxy-setup-go:1.3.1","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSERVICE_SERVER","value":"edgex-vault"},{"name":"KONGURL_SERVER","value":"kong"},{"name":"SECRETSERVICE_SNIS","value":"edgex-kong"},{"name":"SECRETSERVICE_CACERTPATH","value":"/tmp/edgex/secrets/ca/ca.pem"},{"name":"SECRETSERVICE_TOKENPATH","value":"/tmp/edgex/secrets/edgex-security-proxy-setup/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"consul-scripts","mountPath":"/consul/scripts"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-proxy"}},"strategy":{}}},{"name":"edgex-app-service-configurable-rules","service":{"ports":[{"name":"tcp-48100","protocol":"TCP","port":48100,"targetPort":48100}],"selector":{"app":"edgex-app-service-configurable-rules"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-service-configurable-rules"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-service-configurable-rules"}},"spec":{"containers":[{"name":"edgex-app-service-configurable-rules","image":"openyurt/docker-app-service-configurable:1.3.1","ports":[{"name":"tcp-48100","containerPort":48100,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"BINDING_PUBLISHTOPIC","value":"events"},{"name":"SERVICE_HOST","value":"edgex-app-service-configurable-rules"},{"name":"MESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-core-data"},{"name":"SERVICE_PORT","value":"48100"},{"name":"EDGEX_PROFILE","value":"rules-engine"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-service-configurable-rules"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-48081","protocol":"TCP","port":48081,"targetPort":48081}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/docker-core-metadata-go:1.3.1","ports":[{"name":"tcp-48081","containerPort":48081,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-metadata"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-core-metadata/secrets-token.json"},{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}}]}]} \ No newline at end of file diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml b/pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml new file mode 100644 index 00000000000..e079f05652f --- /dev/null +++ b/pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml @@ -0,0 +1,9 @@ +updated: "false" +count: 5 +latestVersion: levski +versions: +- levski +- jakarta +- kamakura +- ireland +- hanoi diff --git a/pkg/controller/platformadmin/config/types.go b/pkg/controller/platformadmin/config/types.go new file mode 100644 index 00000000000..e3727da1a9c --- /dev/null +++ b/pkg/controller/platformadmin/config/types.go @@ -0,0 +1,107 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "embed" + "encoding/json" + "path/filepath" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/klog/v2" +) + +type EdgeXConfig struct { + Versions []*Version `yaml:"versions,omitempty" json:"versions,omitempty"` +} + +type Version struct { + Name string `yaml:"versionName" json:"versionName"` + ConfigMaps []corev1.ConfigMap `yaml:"configMaps,omitempty" json:"configMaps,omitempty"` + Components []*Component `yaml:"components,omitempty" json:"components,omitempty"` +} + +type Component struct { + Name string `yaml:"name" json:"name"` + Service *corev1.ServiceSpec `yaml:"service,omitempty" json:"service,omitempty"` + Deployment *appsv1.DeploymentSpec `yaml:"deployment,omitempty" json:"deployment,omitempty"` +} + +var ( + //go:embed EdgeXConfig + EdgeXFS embed.FS + ManifestPath = filepath.Join(folder, "manifest.yaml") +) + +var ( + folder = "EdgeXConfig/" + securityFile = filepath.Join(folder, "config.json") + nosectyFile = filepath.Join(folder, "config-nosecty.json") +) + +// PlatformAdminControllerConfiguration contains elements describing PlatformAdminController. +type PlatformAdminControllerConfiguration struct { + SecurityComponents map[string][]*Component + NoSectyComponents map[string][]*Component + SecurityConfigMaps map[string][]corev1.ConfigMap + NoSectyConfigMaps map[string][]corev1.ConfigMap +} + +func NewPlatformAdminControllerConfiguration() *PlatformAdminControllerConfiguration { + var ( + edgexconfig = EdgeXConfig{} + edgexnosectyconfig = EdgeXConfig{} + conf = PlatformAdminControllerConfiguration{ + SecurityComponents: make(map[string][]*Component), + NoSectyComponents: make(map[string][]*Component), + SecurityConfigMaps: make(map[string][]corev1.ConfigMap), + NoSectyConfigMaps: make(map[string][]corev1.ConfigMap), + } + ) + + securityContent, err := EdgeXFS.ReadFile(securityFile) + if err != nil { + klog.Errorf("Fail to open the embed EdgeX security config: %v", err) + return nil + } + nosectyContent, err := EdgeXFS.ReadFile(nosectyFile) + if err != nil { + klog.Errorf("Fail to open the embed EdgeX nosecty config: %v", err) + return nil + } + + if err = json.Unmarshal(securityContent, &edgexconfig); err != nil { + klog.Errorf("Fail to unmarshal the embed EdgeX security config: %v", err) + return nil + } + for _, version := range edgexconfig.Versions { + conf.SecurityComponents[version.Name] = version.Components + conf.SecurityConfigMaps[version.Name] = version.ConfigMaps + } + + if err := json.Unmarshal(nosectyContent, &edgexnosectyconfig); err != nil { + klog.Errorf("Fail to unmarshal the embed EdgeX nosecty config: %v", err) + return nil + } + for _, version := range edgexnosectyconfig.Versions { + conf.NoSectyComponents[version.Name] = version.Components + conf.NoSectyConfigMaps[version.Name] = version.ConfigMaps + } + + return &conf +} diff --git a/pkg/controller/platformadmin/platformadmin_controller.go b/pkg/controller/platformadmin/platformadmin_controller.go new file mode 100644 index 00000000000..550f85e48ef --- /dev/null +++ b/pkg/controller/platformadmin/platformadmin_controller.go @@ -0,0 +1,602 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package platformadmin + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "time" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" + "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" + util "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" + utilclient "github.com/openyurtio/openyurt/pkg/util/client" + utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" +) + +func init() { + flag.IntVar(&concurrentReconciles, "platformadmin-workers", concurrentReconciles, "Max concurrent workers for PlatformAdmin controller.") +} + +var ( + concurrentReconciles = 3 + controllerKind = iotv1alpha2.SchemeGroupVersion.WithKind("PlatformAdmin") +) + +const ( + ControllerName = "PlatformAdmin" + + LabelConfigmap = "Configmap" + LabelService = "Service" + LabelDeployment = "Deployment" + + AnnotationServiceTopologyKey = "openyurt.io/topologyKeys" + AnnotationServiceTopologyValueNodePool = "openyurt.io/nodepool" + + ConfigMapName = "common-variables" +) + +func Format(format string, args ...interface{}) string { + s := fmt.Sprintf(format, args...) + return fmt.Sprintf("%s: %s", ControllerName, s) +} + +// ReconcilePlatformAdmin reconciles a PlatformAdmin object +type ReconcilePlatformAdmin struct { + client.Client + scheme *runtime.Scheme + recorder record.EventRecorder + Configration config.PlatformAdminControllerConfiguration +} + +var _ reconcile.Reconciler = &ReconcilePlatformAdmin{} + +// Add creates a new PlatformAdmin Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { + if !utildiscovery.DiscoverGVK(controllerKind) { + return nil + } + + klog.Infof("platformadmin-controller add controller %s", controllerKind.String()) + return add(mgr, newReconciler(c, mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { + return &ReconcilePlatformAdmin{ + Client: utilclient.NewClientFromManager(mgr, ControllerName), + scheme: mgr.GetScheme(), + recorder: mgr.GetEventRecorderFor(ControllerName), + Configration: c.ComponentConfig.PlatformAdminController, + } +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New(ControllerName, mgr, controller.Options{ + Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, + }) + if err != nil { + return err + } + + // Watch for changes to PlatformAdmin + err = c.Watch(&source.Kind{Type: &iotv1alpha2.PlatformAdmin{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &handler.EnqueueRequestForOwner{ + IsController: false, + OwnerType: &iotv1alpha2.PlatformAdmin{}, + }) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &corev1.Service{}}, &handler.EnqueueRequestForOwner{ + IsController: false, + OwnerType: &iotv1alpha2.PlatformAdmin{}, + }) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &appsv1alpha1.YurtAppSet{}}, &handler.EnqueueRequestForOwner{ + IsController: false, + OwnerType: &iotv1alpha2.PlatformAdmin{}, + }) + if err != nil { + return err + } + + klog.V(4).Info("registering the field indexers of platformadmin controller") + if err := util.RegisterFieldIndexers(mgr.GetFieldIndexer()); err != nil { + klog.Errorf("failed to register field indexers for platformadmin controller, %v", err) + return nil + } + + return nil +} + +// +kubebuilder:rbac:groups=iot.openyurt.io,resources=platformadmins,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=iot.openyurt.io,resources=platformadmins/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=iot.openyurt.io,resources=platformadmins/finalizers,verbs=update +// +kubebuilder:rbac:groups=apps.openyurt.io,resources=yurtappsets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps.openyurt.io,resources=yurtappsets/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=core,resources=configmaps;services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=configmaps/status;services/status,verbs=get;update;patch + +// Reconcile reads that state of the cluster for a PlatformAdmin object and makes changes based on the state read +// and what is in the PlatformAdmin.Spec +func (r *ReconcilePlatformAdmin) Reconcile(ctx context.Context, request reconcile.Request) (_ reconcile.Result, reterr error) { + klog.Infof(Format("Reconcile PlatformAdmin %s/%s", request.Namespace, request.Name)) + + // Fetch the PlatformAdmin instance + platformAdmin := &iotv1alpha2.PlatformAdmin{} + if err := r.Get(ctx, request.NamespacedName, platformAdmin); err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + klog.Errorf(Format("Get PlatformAdmin %s/%s error %v", request.Namespace, request.Name, err)) + return reconcile.Result{}, err + } + + platformAdminStatus := platformAdmin.Status.DeepCopy() + isDeleted := false + + // Always issue a patch when exiting this function so changes to the + // resource are patched back to the API server. + defer func(isDeleted *bool) { + if !*isDeleted { + platformAdmin.Status = *platformAdminStatus + + if err := r.Status().Update(ctx, platformAdmin); err != nil { + klog.Errorf(Format("Update the status of PlatformAdmin %s/%s failed", platformAdmin.Namespace, platformAdmin.Name)) + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + + if reterr != nil { + klog.ErrorS(reterr, Format("Reconcile PlatformAdmin %s/%s failed", platformAdmin.Namespace, platformAdmin.Name)) + } + } + }(&isDeleted) + + if platformAdmin.DeletionTimestamp != nil { + isDeleted = true + return r.reconcileDelete(ctx, platformAdmin) + } + + return r.reconcileNormal(ctx, platformAdmin, platformAdminStatus) +} + +func (r *ReconcilePlatformAdmin) reconcileDelete(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin) (reconcile.Result, error) { + klog.V(4).Infof(Format("ReconcileDelete PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + yas := &appsv1alpha1.YurtAppSet{} + var desiredComponents []*config.Component + if platformAdmin.Spec.Security { + desiredComponents = r.Configration.SecurityComponents[platformAdmin.Spec.Version] + } else { + desiredComponents = r.Configration.NoSectyComponents[platformAdmin.Spec.Version] + } + + additionalComponents, err := annotationToComponent(platformAdmin.Annotations) + if err != nil { + klog.Errorf(Format("annotationToComponent error %v", err)) + return reconcile.Result{}, err + } + desiredComponents = append(desiredComponents, additionalComponents...) + + //TODO: handle PlatformAdmin.Spec.Components + + for _, dc := range desiredComponents { + if err := r.Get( + ctx, + types.NamespacedName{Namespace: platformAdmin.Namespace, Name: dc.Name}, + yas); err != nil { + klog.V(4).ErrorS(err, Format("Get YurtAppSet %s/%s error", platformAdmin.Namespace, dc.Name)) + continue + } + + oldYas := yas.DeepCopy() + + for i, pool := range yas.Spec.Topology.Pools { + if pool.Name == platformAdmin.Spec.PoolName { + yas.Spec.Topology.Pools[i] = yas.Spec.Topology.Pools[len(yas.Spec.Topology.Pools)-1] + yas.Spec.Topology.Pools = yas.Spec.Topology.Pools[:len(yas.Spec.Topology.Pools)-1] + } + } + if err := r.Client.Patch(ctx, yas, client.MergeFrom(oldYas)); err != nil { + klog.V(4).ErrorS(err, Format("Patch YurtAppSet %s/%s error", platformAdmin.Namespace, dc.Name)) + return reconcile.Result{}, err + } + } + + controllerutil.RemoveFinalizer(platformAdmin, iotv1alpha2.PlatformAdminFinalizer) + if err := r.Client.Update(ctx, platformAdmin); err != nil { + klog.Errorf(Format("Update PlatformAdmin %s error %v", klog.KObj(platformAdmin), err)) + return reconcile.Result{}, err + } + + return reconcile.Result{}, nil +} + +func (r *ReconcilePlatformAdmin) reconcileNormal(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminStatus *iotv1alpha2.PlatformAdminStatus) (reconcile.Result, error) { + klog.V(4).Infof(Format("ReconcileNormal PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + controllerutil.AddFinalizer(platformAdmin, iotv1alpha2.PlatformAdminFinalizer) + + platformAdmin.Status.Initialized = true + klog.V(4).Infof(Format("ReconcileConfigmap PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + if ok, err := r.reconcileConfigmap(ctx, platformAdmin, platformAdminStatus); !ok { + if err != nil { + util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ConfigmapAvailableCondition, corev1.ConditionFalse, iotv1alpha2.ConfigmapProvisioningFailedReason, err.Error())) + return reconcile.Result{}, errors.Wrapf(err, + "unexpected error while reconciling configmap for %s", platformAdmin.Namespace+"/"+platformAdmin.Name) + } + util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ConfigmapAvailableCondition, corev1.ConditionFalse, iotv1alpha2.ConfigmapProvisioningReason, "")) + return reconcile.Result{RequeueAfter: 10 * time.Second}, nil + } + util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ConfigmapAvailableCondition, corev1.ConditionTrue, "", "")) + + klog.V(4).Infof(Format("ReconcileComponent PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + if ok, err := r.reconcileComponent(ctx, platformAdmin, platformAdminStatus); !ok { + if err != nil { + util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ComponentAvailableCondition, corev1.ConditionFalse, iotv1alpha2.ComponentProvisioningReason, err.Error())) + return reconcile.Result{}, errors.Wrapf(err, + "unexpected error while reconciling component for %s", platformAdmin.Namespace+"/"+platformAdmin.Name) + } + util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ComponentAvailableCondition, corev1.ConditionFalse, iotv1alpha2.ComponentProvisioningReason, "")) + return reconcile.Result{RequeueAfter: 10 * time.Second}, nil + } + util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ComponentAvailableCondition, corev1.ConditionTrue, "", "")) + + platformAdminStatus.Ready = true + if err := r.Client.Update(ctx, platformAdmin); err != nil { + klog.Errorf(Format("Update PlatformAdmin %s error %v", klog.KObj(platformAdmin), err)) + return reconcile.Result{}, err + } + + return reconcile.Result{}, nil +} + +func (r *ReconcilePlatformAdmin) reconcileConfigmap(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, _ *iotv1alpha2.PlatformAdminStatus) (bool, error) { + var configmaps []corev1.ConfigMap + needConfigMaps := make(map[string]struct{}) + + if platformAdmin.Spec.Security { + configmaps = r.Configration.SecurityConfigMaps[platformAdmin.Spec.Version] + } else { + configmaps = r.Configration.NoSectyConfigMaps[platformAdmin.Spec.Version] + } + for _, configmap := range configmaps { + // Supplement runtime information + configmap.Namespace = platformAdmin.Namespace + configmap.Labels = make(map[string]string) + configmap.Labels[iotv1alpha2.LabelPlatformAdminGenerate] = LabelConfigmap + + _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &configmap, func() error { + return controllerutil.SetOwnerReference(platformAdmin, &configmap, (r.Scheme())) + }) + if err != nil { + return false, err + } + + needConfigMaps[configmap.Name] = struct{}{} + } + + configmaplist := &corev1.ConfigMapList{} + if err := r.List(ctx, configmaplist, client.InNamespace(platformAdmin.Namespace), client.MatchingLabels{iotv1alpha2.LabelPlatformAdminGenerate: LabelConfigmap}); err == nil { + for _, c := range configmaplist.Items { + if _, ok := needConfigMaps[c.Name]; !ok { + r.removeOwner(ctx, platformAdmin, &c) + } + } + } + + return true, nil +} + +func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminStatus *iotv1alpha2.PlatformAdminStatus) (bool, error) { + var desireComponents []*config.Component + needComponents := make(map[string]struct{}) + var readyComponent int32 = 0 + + if platformAdmin.Spec.Security { + desireComponents = r.Configration.SecurityComponents[platformAdmin.Spec.Version] + } else { + desireComponents = r.Configration.NoSectyComponents[platformAdmin.Spec.Version] + } + + additionalComponents, err := annotationToComponent(platformAdmin.Annotations) + if err != nil { + return false, err + } + desireComponents = append(desireComponents, additionalComponents...) + + //TODO: handle PlatformAdmin.Spec.Components + + defer func() { + platformAdminStatus.ReadyComponentNum = readyComponent + platformAdminStatus.UnreadyComponentNum = int32(len(desireComponents)) - readyComponent + }() + +NextC: + for _, desireComponent := range desireComponents { + readyService := false + readyDeployment := false + needComponents[desireComponent.Name] = struct{}{} + + if _, err := r.handleService(ctx, platformAdmin, desireComponent); err != nil { + return false, err + } + readyService = true + + yas := &appsv1alpha1.YurtAppSet{} + err := r.Get( + ctx, + types.NamespacedName{ + Namespace: platformAdmin.Namespace, + Name: desireComponent.Name}, + yas) + if err != nil { + if !apierrors.IsNotFound(err) { + return false, err + } + _, err = r.handleYurtAppSet(ctx, platformAdmin, desireComponent) + if err != nil { + return false, err + } + } else { + oldYas := yas.DeepCopy() + + if _, ok := yas.Status.PoolReplicas[platformAdmin.Spec.PoolName]; ok { + if yas.Status.ReadyReplicas == yas.Status.Replicas { + readyDeployment = true + if readyDeployment && readyService { + readyComponent++ + } + } + continue NextC + } + pool := appsv1alpha1.Pool{ + Name: platformAdmin.Spec.PoolName, + Replicas: pointer.Int32Ptr(1), + } + pool.NodeSelectorTerm.MatchExpressions = append(pool.NodeSelectorTerm.MatchExpressions, + corev1.NodeSelectorRequirement{ + Key: appsv1alpha1.LabelCurrentNodePool, + Operator: corev1.NodeSelectorOpIn, + Values: []string{platformAdmin.Spec.PoolName}, + }) + flag := false + for _, up := range yas.Spec.Topology.Pools { + if up.Name == pool.Name { + flag = true + break + } + } + if !flag { + yas.Spec.Topology.Pools = append(yas.Spec.Topology.Pools, pool) + } + if err := controllerutil.SetOwnerReference(platformAdmin, yas, r.Scheme()); err != nil { + return false, err + } + if err := r.Client.Patch(ctx, yas, client.MergeFrom(oldYas)); err != nil { + klog.Errorf(Format("Patch yurtappset %s/%s failed: %v", yas.Namespace, yas.Name, err)) + return false, err + } + } + } + + // Remove the service owner that we do not need + servicelist := &corev1.ServiceList{} + if err := r.List(ctx, servicelist, client.InNamespace(platformAdmin.Namespace), client.MatchingLabels{iotv1alpha2.LabelPlatformAdminGenerate: LabelService}); err == nil { + for _, s := range servicelist.Items { + if _, ok := needComponents[s.Name]; !ok { + r.removeOwner(ctx, platformAdmin, &s) + } + } + } + + // Remove the yurtappset owner that we do not need + yurtappsetlist := &appsv1alpha1.YurtAppSetList{} + if err := r.List(ctx, yurtappsetlist, client.InNamespace(platformAdmin.Namespace), client.MatchingLabels{iotv1alpha2.LabelPlatformAdminGenerate: LabelDeployment}); err == nil { + for _, s := range yurtappsetlist.Items { + if _, ok := needComponents[s.Name]; !ok { + r.removeOwner(ctx, platformAdmin, &s) + } + } + } + + return readyComponent == int32(len(desireComponents)), nil +} + +func (r *ReconcilePlatformAdmin) handleService(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, component *config.Component) (*corev1.Service, error) { + // It is possible that the component does not need service. + // Therefore, you need to be careful when calling this function. + // It is still possible for service to be nil when there is no error! + if component.Service == nil { + return nil, nil + } + + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Labels: make(map[string]string), + Annotations: make(map[string]string), + Name: component.Name, + Namespace: platformAdmin.Namespace, + }, + Spec: *component.Service, + } + service.Labels[iotv1alpha2.LabelPlatformAdminGenerate] = LabelService + service.Annotations[AnnotationServiceTopologyKey] = AnnotationServiceTopologyValueNodePool + + _, err := controllerutil.CreateOrUpdate( + ctx, + r.Client, + service, + func() error { + return controllerutil.SetOwnerReference(platformAdmin, service, r.Scheme()) + }, + ) + + if err != nil { + return nil, err + } + return service, nil +} + +func (r *ReconcilePlatformAdmin) handleYurtAppSet(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, component *config.Component) (*appsv1alpha1.YurtAppSet, error) { + yas := &appsv1alpha1.YurtAppSet{ + ObjectMeta: metav1.ObjectMeta{ + Labels: make(map[string]string), + Annotations: make(map[string]string), + Name: component.Name, + Namespace: platformAdmin.Namespace, + }, + Spec: appsv1alpha1.YurtAppSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": component.Name}, + }, + WorkloadTemplate: appsv1alpha1.WorkloadTemplate{ + DeploymentTemplate: &appsv1alpha1.DeploymentTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": component.Name}, + }, + Spec: *component.Deployment, + }, + }, + }, + } + + yas.Labels[iotv1alpha2.LabelPlatformAdminGenerate] = LabelDeployment + pool := appsv1alpha1.Pool{ + Name: platformAdmin.Spec.PoolName, + Replicas: pointer.Int32Ptr(1), + } + pool.NodeSelectorTerm.MatchExpressions = append(pool.NodeSelectorTerm.MatchExpressions, + corev1.NodeSelectorRequirement{ + Key: appsv1alpha1.LabelCurrentNodePool, + Operator: corev1.NodeSelectorOpIn, + Values: []string{platformAdmin.Spec.PoolName}, + }) + yas.Spec.Topology.Pools = append(yas.Spec.Topology.Pools, pool) + if err := controllerutil.SetControllerReference(platformAdmin, yas, r.Scheme()); err != nil { + return nil, err + } + if err := r.Create(ctx, yas); err != nil { + return nil, err + } + return yas, nil +} + +func (r *ReconcilePlatformAdmin) removeOwner(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, obj client.Object) error { + owners := obj.GetOwnerReferences() + + for i, owner := range owners { + if owner.UID == platformAdmin.UID { + owners[i] = owners[len(owners)-1] + owners = owners[:len(owners)-1] + + if len(owners) == 0 { + return r.Delete(ctx, obj) + } else { + obj.SetOwnerReferences(owners) + return r.Update(ctx, obj) + } + } + } + return nil +} + +// For version compatibility, v1alpha1's additionalservice and additionaldeployment are placed in +// v2alpha2's annotation, this function is to convert the annotation to component. +func annotationToComponent(annotation map[string]string) ([]*config.Component, error) { + var components []*config.Component = []*config.Component{} + var additionalDeployments []iotv1alpha1.DeploymentTemplateSpec = make([]iotv1alpha1.DeploymentTemplateSpec, 0) + if _, ok := annotation["AdditionalDeployments"]; ok { + err := json.Unmarshal([]byte(annotation["AdditionalDeployments"]), &additionalDeployments) + if err != nil { + return nil, err + } + } + var additionalServices []iotv1alpha1.ServiceTemplateSpec = make([]iotv1alpha1.ServiceTemplateSpec, 0) + if _, ok := annotation["AdditionalServices"]; ok { + err := json.Unmarshal([]byte(annotation["AdditionalServices"]), &additionalServices) + if err != nil { + return nil, err + } + } + if len(additionalDeployments) == 0 && len(additionalServices) == 0 { + return components, nil + } + var services map[string]*corev1.ServiceSpec = make(map[string]*corev1.ServiceSpec) + var usedServices map[string]struct{} = make(map[string]struct{}) + for _, additionalservice := range additionalServices { + services[additionalservice.Name] = &additionalservice.Spec + } + for _, additionalDeployment := range additionalDeployments { + var component config.Component + component.Name = additionalDeployment.Name + component.Deployment = &additionalDeployment.Spec + service, ok := services[component.Name] + if ok { + component.Service = service + usedServices[component.Name] = struct{}{} + } + components = append(components, &component) + } + if len(usedServices) < len(services) { + for name, service := range services { + _, ok := usedServices[name] + if ok { + continue + } + var component config.Component + component.Name = name + component.Service = service + components = append(components, &component) + } + } + + return components, nil +} diff --git a/pkg/controller/platformadmin/utils/fieldindexer.go b/pkg/controller/platformadmin/utils/fieldindexer.go new file mode 100644 index 00000000000..c033ec1ee52 --- /dev/null +++ b/pkg/controller/platformadmin/utils/fieldindexer.go @@ -0,0 +1,50 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "sync" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" +) + +const ( + IndexerPathForNodepool = "spec.poolName" +) + +var registerOnce sync.Once + +func RegisterFieldIndexers(fi client.FieldIndexer) error { + var err error + registerOnce.Do(func() { + // register the fieldIndexer for device + if err = fi.IndexField(context.TODO(), &v1alpha2.PlatformAdmin{}, IndexerPathForNodepool, func(rawObj client.Object) []string { + platformAdmin, ok := rawObj.(*v1alpha2.PlatformAdmin) + if ok { + return []string{platformAdmin.Spec.PoolName} + } + return []string{} + }); err != nil { + return + } + }) + + return err +} diff --git a/pkg/controller/platformadmin/utils/util.go b/pkg/controller/platformadmin/utils/util.go new file mode 100644 index 00000000000..eb9deb1ae69 --- /dev/null +++ b/pkg/controller/platformadmin/utils/util.go @@ -0,0 +1,72 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" +) + +// NewPlatformAdminCondition creates a new PlatformAdmin condition. +func NewPlatformAdminCondition(condType iotv1alpha2.PlatformAdminConditionType, status corev1.ConditionStatus, reason, message string) *iotv1alpha2.PlatformAdminCondition { + return &iotv1alpha2.PlatformAdminCondition{ + Type: condType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } +} + +// GetPlatformAdminCondition returns the condition with the provided type. +func GetPlatformAdminCondition(status iotv1alpha2.PlatformAdminStatus, condType iotv1alpha2.PlatformAdminConditionType) *iotv1alpha2.PlatformAdminCondition { + for i := range status.Conditions { + c := status.Conditions[i] + if c.Type == condType { + return &c + } + } + return nil +} + +// SetPlatformAdminCondition updates the PlatformAdmin to include the provided condition. If the condition that +// we are about to add already exists and has the same status, reason and message then we are not going to update. +func SetPlatformAdminCondition(status *iotv1alpha2.PlatformAdminStatus, condition *iotv1alpha2.PlatformAdminCondition) { + currentCond := GetPlatformAdminCondition(*status, condition.Type) + if currentCond != nil && currentCond.Status == condition.Status && currentCond.Reason == condition.Reason { + return + } + + if currentCond != nil && currentCond.Status == condition.Status { + condition.LastTransitionTime = currentCond.LastTransitionTime + } + newConditions := filterOutCondition(status.Conditions, condition.Type) + status.Conditions = append(newConditions, *condition) +} + +func filterOutCondition(conditions []iotv1alpha2.PlatformAdminCondition, condType iotv1alpha2.PlatformAdminConditionType) []iotv1alpha2.PlatformAdminCondition { + var newConditions []iotv1alpha2.PlatformAdminCondition + for _, c := range conditions { + if c.Type == condType { + continue + } + newConditions = append(newConditions, c) + } + return newConditions +} diff --git a/pkg/webhook/platformadmin/v1alpha1/platformadmin_default.go b/pkg/webhook/platformadmin/v1alpha1/platformadmin_default.go new file mode 100644 index 00000000000..c95eb92e71c --- /dev/null +++ b/pkg/webhook/platformadmin/v1alpha1/platformadmin_default.go @@ -0,0 +1,43 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +// Default satisfies the defaulting webhook interface. +func (webhook *PlatformAdminHandler) Default(ctx context.Context, obj runtime.Object) error { + platformAdmin, ok := obj.(*v1alpha1.PlatformAdmin) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a PlatformAdmin but got a %T", obj)) + } + + v1alpha1.SetDefaultsPlatformAdmin(platformAdmin) + + if platformAdmin.Spec.Version == "" { + platformAdmin.Spec.Version = webhook.Manifests.LatestVersion + } + + return nil +} diff --git a/pkg/webhook/platformadmin/v1alpha1/platformadmin_handler.go b/pkg/webhook/platformadmin/v1alpha1/platformadmin_handler.go new file mode 100644 index 00000000000..83a7e4e3e92 --- /dev/null +++ b/pkg/webhook/platformadmin/v1alpha1/platformadmin_handler.go @@ -0,0 +1,86 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "gopkg.in/yaml.v3" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" + "github.com/openyurtio/openyurt/pkg/webhook/util" +) + +type Manifest struct { + Updated string `yaml:"updated"` + Count int `yaml:"count"` + LatestVersion string `yaml:"latestVersion"` + Versions []string `yaml:"versions"` +} + +// SetupWebhookWithManager sets up Cluster webhooks. +func (webhook *PlatformAdminHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { + // init + webhook.Client = mgr.GetClient() + + gvk, err := apiutil.GVKForObject(&v1alpha1.PlatformAdmin{}, mgr.GetScheme()) + if err != nil { + return "", "", err + } + + if err := webhook.initManifest(); err != nil { + return "", "", err + } + + return util.GenerateMutatePath(gvk), + util.GenerateValidatePath(gvk), + ctrl.NewWebhookManagedBy(mgr). + For(&v1alpha1.PlatformAdmin{}). + WithDefaulter(webhook). + WithValidator(webhook). + Complete() +} + +func (webhook *PlatformAdminHandler) initManifest() error { + webhook.Manifests = &Manifest{} + + manifestContent, err := config.EdgeXFS.ReadFile(config.ManifestPath) + if err != nil { + klog.Error(err, "File to open the embed EdgeX manifest file") + return err + } + + if err := yaml.Unmarshal(manifestContent, webhook.Manifests); err != nil { + klog.Error(err, "Error manifest EdgeX configuration file") + return err + } + + return nil +} + +// Cluster implements a validating and defaulting webhook for Cluster. +type PlatformAdminHandler struct { + Client client.Client + Manifests *Manifest +} + +var _ webhook.CustomDefaulter = &PlatformAdminHandler{} +var _ webhook.CustomValidator = &PlatformAdminHandler{} diff --git a/pkg/webhook/platformadmin/v1alpha1/platformadmin_validation.go b/pkg/webhook/platformadmin/v1alpha1/platformadmin_validation.go new file mode 100644 index 00000000000..538017c4628 --- /dev/null +++ b/pkg/webhook/platformadmin/v1alpha1/platformadmin_validation.go @@ -0,0 +1,118 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + + unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + util "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" +) + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *PlatformAdminHandler) ValidateCreate(ctx context.Context, obj runtime.Object) error { + platformAdmin, ok := obj.(*v1alpha1.PlatformAdmin) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a PlatformAdmin but got a %T", obj)) + } + + //validate + if allErrs := webhook.validate(ctx, platformAdmin); len(allErrs) > 0 { + return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind("PlatformAdmin").GroupKind(), platformAdmin.Name, allErrs) + } + + return nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *PlatformAdminHandler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error { + newPlatformAdmin, ok := newObj.(*v1alpha1.PlatformAdmin) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a PlatformAdmin but got a %T", newObj)) + } + oldPlatformAdmin, ok := oldObj.(*v1alpha1.PlatformAdmin) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a PlatformAdmin but got a %T", oldObj)) + } + + // validate + newErrorList := webhook.validate(ctx, newPlatformAdmin) + oldErrorList := webhook.validate(ctx, oldPlatformAdmin) + if allErrs := append(newErrorList, oldErrorList...); len(allErrs) > 0 { + return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind("PlatformAdmin").GroupKind(), newPlatformAdmin.Name, allErrs) + } + return nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *PlatformAdminHandler) ValidateDelete(_ context.Context, obj runtime.Object) error { + return nil +} + +func (webhook *PlatformAdminHandler) validate(ctx context.Context, platformAdmin *v1alpha1.PlatformAdmin) field.ErrorList { + // verify that the poolname nodepool + if nodePoolErrs := webhook.validatePlatformAdminWithNodePools(ctx, platformAdmin); nodePoolErrs != nil { + return nodePoolErrs + } + return nil +} + +func (webhook *PlatformAdminHandler) validatePlatformAdminWithNodePools(ctx context.Context, platformAdmin *v1alpha1.PlatformAdmin) field.ErrorList { + // verify that the poolname is a right nodepool name + nodePools := &unitv1alpha1.NodePoolList{} + if err := webhook.Client.List(ctx, nodePools); err != nil { + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "poolName"), platformAdmin.Spec.PoolName, "can not list nodepools, cause"+err.Error()), + } + } + ok := false + for _, nodePool := range nodePools.Items { + if nodePool.ObjectMeta.Name == platformAdmin.Spec.PoolName { + ok = true + break + } + } + if !ok { + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "poolName"), platformAdmin.Spec.PoolName, "can not find the nodepool"), + } + } + // verify that no other platformadmin in the nodepool + var platformadmins v1alpha1.PlatformAdminList + listOptions := client.MatchingFields{util.IndexerPathForNodepool: platformAdmin.Spec.PoolName} + if err := webhook.Client.List(ctx, &platformadmins, listOptions); err != nil { + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "poolName"), platformAdmin.Spec.PoolName, "can not list platformadmins, cause"+err.Error()), + } + } + for _, other := range platformadmins.Items { + if platformAdmin.Name != other.Name { + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "poolName"), platformAdmin.Spec.PoolName, "already used by other platformadmin instance,"), + } + } + } + + return nil +} diff --git a/pkg/webhook/platformadmin/v1alpha2/platformadmin_default.go b/pkg/webhook/platformadmin/v1alpha2/platformadmin_default.go new file mode 100644 index 00000000000..f0e26875acc --- /dev/null +++ b/pkg/webhook/platformadmin/v1alpha2/platformadmin_default.go @@ -0,0 +1,47 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" +) + +// Default satisfies the defaulting webhook interface. +func (webhook *PlatformAdminHandler) Default(ctx context.Context, obj runtime.Object) error { + platformAdmin, ok := obj.(*v1alpha2.PlatformAdmin) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a PlatformAdmin but got a %T", obj)) + } + + v1alpha2.SetDefaultsPlatformAdmin(platformAdmin) + + if platformAdmin.Spec.Version == "" { + platformAdmin.Spec.Version = webhook.Manifests.LatestVersion + } + + if platformAdmin.Spec.Platform == "" { + platformAdmin.Spec.Platform = v1alpha2.PlatformAdminPlatformEdgeX + } + + return nil +} diff --git a/pkg/webhook/platformadmin/v1alpha2/platformadmin_handler.go b/pkg/webhook/platformadmin/v1alpha2/platformadmin_handler.go new file mode 100644 index 00000000000..a6b193a0b74 --- /dev/null +++ b/pkg/webhook/platformadmin/v1alpha2/platformadmin_handler.go @@ -0,0 +1,89 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "gopkg.in/yaml.v3" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" + "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" + "github.com/openyurtio/openyurt/pkg/webhook/util" +) + +type Manifest struct { + Updated string `yaml:"updated"` + Count int `yaml:"count"` + LatestVersion string `yaml:"latestVersion"` + Versions []string `yaml:"versions"` +} + +// SetupWebhookWithManager sets up Cluster webhooks. +func (webhook *PlatformAdminHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { + // init + webhook.Client = mgr.GetClient() + + gvk, err := apiutil.GVKForObject(&v1alpha2.PlatformAdmin{}, mgr.GetScheme()) + if err != nil { + return "", "", err + } + + if err := webhook.initManifest(); err != nil { + return "", "", err + } + + return util.GenerateMutatePath(gvk), + util.GenerateValidatePath(gvk), + ctrl.NewWebhookManagedBy(mgr). + For(&v1alpha2.PlatformAdmin{}). + WithDefaulter(webhook). + WithValidator(webhook). + Complete() +} + +func (webhook *PlatformAdminHandler) initManifest() error { + webhook.Manifests = &Manifest{} + + manifestContent, err := config.EdgeXFS.ReadFile(config.ManifestPath) + if err != nil { + klog.Error(err, "File to open the embed EdgeX manifest file") + return err + } + + if err := yaml.Unmarshal(manifestContent, webhook.Manifests); err != nil { + klog.Error(err, "Error manifest EdgeX configuration file") + return err + } + + return nil +} + +// +kubebuilder:webhook:path=/validate-iot-openyurt-io-v1alpha2-platformadmin,mutating=false,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1,groups=iot.openyurt.io,resources=platformadmins,verbs=create;update,versions=v1alpha2,name=vplatformadmin.kb.io +// +kubebuilder:webhook:path=/mutate-iot-openyurt-io-v1alpha2-platformadmin,mutating=true,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1,groups=iot.openyurt.io,resources=platformadmins,verbs=create;update,versions=v1alpha2,name=mplatformadmin.kb.io + +// Cluster implements a validating and defaulting webhook for Cluster. +type PlatformAdminHandler struct { + Client client.Client + Manifests *Manifest +} + +var _ webhook.CustomDefaulter = &PlatformAdminHandler{} +var _ webhook.CustomValidator = &PlatformAdminHandler{} diff --git a/pkg/webhook/platformadmin/v1alpha2/platformadmin_validation.go b/pkg/webhook/platformadmin/v1alpha2/platformadmin_validation.go new file mode 100644 index 00000000000..170c790244a --- /dev/null +++ b/pkg/webhook/platformadmin/v1alpha2/platformadmin_validation.go @@ -0,0 +1,144 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha2 + +import ( + "context" + "fmt" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + + unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" + util "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" +) + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *PlatformAdminHandler) ValidateCreate(ctx context.Context, obj runtime.Object) error { + platformAdmin, ok := obj.(*v1alpha2.PlatformAdmin) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a PlatformAdmin but got a %T", obj)) + } + + //validate + if allErrs := webhook.validate(ctx, platformAdmin); len(allErrs) > 0 { + return apierrors.NewInvalid(v1alpha2.GroupVersion.WithKind("PlatformAdmin").GroupKind(), platformAdmin.Name, allErrs) + } + + return nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *PlatformAdminHandler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error { + newPlatformAdmin, ok := newObj.(*v1alpha2.PlatformAdmin) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a PlatformAdmin but got a %T", newObj)) + } + oldPlatformAdmin, ok := oldObj.(*v1alpha2.PlatformAdmin) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a PlatformAdmin but got a %T", oldObj)) + } + + // validate + newErrorList := webhook.validate(ctx, newPlatformAdmin) + oldErrorList := webhook.validate(ctx, oldPlatformAdmin) + if allErrs := append(newErrorList, oldErrorList...); len(allErrs) > 0 { + return apierrors.NewInvalid(v1alpha2.GroupVersion.WithKind("PlatformAdmin").GroupKind(), newPlatformAdmin.Name, allErrs) + } + return nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *PlatformAdminHandler) ValidateDelete(_ context.Context, obj runtime.Object) error { + return nil +} + +func (webhook *PlatformAdminHandler) validate(ctx context.Context, platformAdmin *v1alpha2.PlatformAdmin) field.ErrorList { + + // verify the version + if specErrs := webhook.validatePlatformAdminSpec(platformAdmin); specErrs != nil { + return specErrs + } + // verify that the poolname nodepool + if nodePoolErrs := webhook.validatePlatformAdminWithNodePools(ctx, platformAdmin); nodePoolErrs != nil { + return nodePoolErrs + } + return nil +} + +func (webhook *PlatformAdminHandler) validatePlatformAdminSpec(platformAdmin *v1alpha2.PlatformAdmin) field.ErrorList { + // TODO: Need to divert traffic based on the type of platform + + // Verify that the platform is supported + if platformAdmin.Spec.Platform != v1alpha2.PlatformAdminPlatformEdgeX { + return field.ErrorList{field.Invalid(field.NewPath("spec", "platform"), platformAdmin.Spec.Platform, "must be "+v1alpha2.PlatformAdminPlatformEdgeX)} + } + + // Verify that it is a supported platformadmin version + for _, version := range webhook.Manifests.Versions { + if platformAdmin.Spec.Version == version { + return nil + } + } + + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "version"), platformAdmin.Spec.Version, "must be one of"+strings.Join(webhook.Manifests.Versions, ",")), + } +} + +func (webhook *PlatformAdminHandler) validatePlatformAdminWithNodePools(ctx context.Context, platformAdmin *v1alpha2.PlatformAdmin) field.ErrorList { + // verify that the poolname is a right nodepool name + nodePools := &unitv1alpha1.NodePoolList{} + if err := webhook.Client.List(ctx, nodePools); err != nil { + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "poolName"), platformAdmin.Spec.PoolName, "can not list nodepools, cause"+err.Error()), + } + } + ok := false + for _, nodePool := range nodePools.Items { + if nodePool.ObjectMeta.Name == platformAdmin.Spec.PoolName { + ok = true + break + } + } + if !ok { + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "poolName"), platformAdmin.Spec.PoolName, "can not find the nodepool"), + } + } + // verify that no other platformadmin in the nodepool + var platformadmins v1alpha2.PlatformAdminList + listOptions := client.MatchingFields{util.IndexerPathForNodepool: platformAdmin.Spec.PoolName} + if err := webhook.Client.List(ctx, &platformadmins, listOptions); err != nil { + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "poolName"), platformAdmin.Spec.PoolName, "can not list platformadmins, cause "+err.Error()), + } + } + for _, other := range platformadmins.Items { + if platformAdmin.Name != other.Name { + return field.ErrorList{ + field.Invalid(field.NewPath("spec", "poolName"), platformAdmin.Spec.PoolName, "already used by other platformadmin instance,"), + } + } + } + + return nil +} diff --git a/pkg/webhook/server.go b/pkg/webhook/server.go index 74a83b8e005..6b1619bcac5 100644 --- a/pkg/webhook/server.go +++ b/pkg/webhook/server.go @@ -27,6 +27,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" "github.com/openyurtio/openyurt/pkg/controller/nodepool" + "github.com/openyurtio/openyurt/pkg/controller/platformadmin" "github.com/openyurtio/openyurt/pkg/controller/raven" ctrlutil "github.com/openyurtio/openyurt/pkg/controller/util" "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon" @@ -35,6 +36,8 @@ import ( v1alpha1gateway "github.com/openyurtio/openyurt/pkg/webhook/gateway/v1alpha1" v1alpha1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1alpha1" v1beta1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1beta1" + v1alpha1platformadmin "github.com/openyurtio/openyurt/pkg/webhook/platformadmin/v1alpha1" + v1alpha2platformadmin "github.com/openyurtio/openyurt/pkg/webhook/platformadmin/v1alpha2" v1pod "github.com/openyurtio/openyurt/pkg/webhook/pod/v1" "github.com/openyurtio/openyurt/pkg/webhook/util" webhookcontroller "github.com/openyurtio/openyurt/pkg/webhook/util/controller" @@ -75,6 +78,8 @@ func init() { addControllerWebhook(yurtstaticset.ControllerName, &v1alpha1yurtstaticset.YurtStaticSetHandler{}) addControllerWebhook(yurtappset.ControllerName, &v1alpha1yurtappset.YurtAppSetHandler{}) addControllerWebhook(yurtappdaemon.ControllerName, &v1alpha1yurtappdaemon.YurtAppDaemonHandler{}) + addControllerWebhook(platformadmin.ControllerName, &v1alpha1platformadmin.PlatformAdminHandler{}) + addControllerWebhook(platformadmin.ControllerName, &v1alpha2platformadmin.PlatformAdminHandler{}) independentWebhooks[v1pod.WebhookName] = &v1pod.PodHandler{} } From 89abaffa40987e33ac37d474582cdd698c5d1791 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Mon, 19 Jun 2023 13:58:12 +0800 Subject: [PATCH 36/93] improve yurthub get pods for cloud node (#1514) --- cmd/yurthub/app/config/config.go | 10 ++++++++- pkg/yurthub/server/server.go | 35 +++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/cmd/yurthub/app/config/config.go b/cmd/yurthub/app/config/config.go index 88f35cfa93a..85a64ab435a 100644 --- a/cmd/yurthub/app/config/config.go +++ b/cmd/yurthub/app/config/config.go @@ -302,12 +302,20 @@ func registerInformers(options *options.YurtHubOptions, if tenantNs != "" { newSecretInformer := func(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return coreinformers.NewFilteredSecretInformer(client, tenantNs, resyncPeriod, nil, nil) } informerFactory.InformerFor(&corev1.Secret{}, newSecretInformer) } + if workingMode == util.WorkingModeCloud { + newPodInformer := func(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + listOptions := func(ops *metav1.ListOptions) { + ops.FieldSelector = fields.Set{"spec.nodeName": options.NodeName}.String() + } + return coreinformers.NewFilteredPodInformer(client, "", resyncPeriod, nil, listOptions) + } + informerFactory.InformerFor(&corev1.Pod{}, newPodInformer) + } } // isServiceTopologyFilterEnabled is used to verify the service topology filter should be enabled or not. diff --git a/pkg/yurthub/server/server.go b/pkg/yurthub/server/server.go index fead050d390..4b7189ac4b6 100644 --- a/pkg/yurthub/server/server.go +++ b/pkg/yurthub/server/server.go @@ -22,12 +22,17 @@ import ( "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus/promhttp" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/informers" + "k8s.io/klog/v2" "github.com/openyurtio/openyurt/cmd/yurthub/app/config" "github.com/openyurtio/openyurt/pkg/profile" "github.com/openyurtio/openyurt/pkg/yurthub/certificate" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/rest" ota "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate" + otautil "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate/util" "github.com/openyurtio/openyurt/pkg/yurthub/util" ) @@ -89,7 +94,11 @@ func registerHandlers(c *mux.Router, cfg *config.YurtHubConfiguration, rest *res c.Handle("/metrics", promhttp.Handler()) // register handler for ota upgrade - c.Handle("/pods", ota.GetPods(cfg.StorageWrapper)).Methods("GET") + if cfg.WorkingMode == util.WorkingModeEdge { + c.Handle("/pods", ota.GetPods(cfg.StorageWrapper)).Methods("GET") + } else { + c.Handle("/pods", getPodList(cfg.SharedFactory, cfg.NodeName)).Methods("GET") + } c.Handle("/openyurt.io/v1/namespaces/{ns}/pods/{podname}/upgrade", ota.HealthyCheck(rest, cfg.NodeName, ota.UpdatePod)).Methods("POST") } @@ -112,3 +121,27 @@ func readyz(certificateMgr certificate.YurtCertificateManager) http.Handler { } }) } + +func getPodList(sharedFactory informers.SharedInformerFactory, nodeName string) http.Handler { + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + podLister := sharedFactory.Core().V1().Pods().Lister() + podList, err := podLister.List(labels.Everything()) + if err != nil { + klog.Errorf("get pods key failed, %v", err) + otautil.WriteErr(w, "Get pods key failed", http.StatusInternalServerError) + return + } + pl := new(corev1.PodList) + for i := range podList { + pl.Items = append(pl.Items, *podList[i]) + } + + data, err := otautil.EncodePods(pl) + if err != nil { + klog.Errorf("Encode pod list failed, %v", err) + otautil.WriteErr(w, "Encode pod list failed", http.StatusInternalServerError) + } + otautil.WriteJSONResponse(w, data) + }) +} From cc68ac5d6f59deff1a8fee61cc668ae73bf0ae93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 14:01:12 +0800 Subject: [PATCH 37/93] build(deps): bump google.golang.org/grpc from 1.55.0 to 1.56.0 (#1548) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.55.0 to 1.56.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.55.0...v1.56.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 14 +++++++------- go.sum | 30 ++++++++++++++++-------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index e938844ec97..330af25c395 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 golang.org/x/sys v0.8.0 - google.golang.org/grpc v1.55.0 + google.golang.org/grpc v1.56.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.1 @@ -65,7 +65,7 @@ require ( ) require ( - cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute v1.19.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/BurntSushi/toml v0.4.1 // indirect @@ -140,15 +140,15 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.5.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/term v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.2 // indirect diff --git a/go.sum b/go.sum index 8deca09fc6d..ae74c11aceb 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -115,6 +115,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/clusterhq/flocker-go v0.0.0-20160920122132-2b8b7259d313/go.mod h1:P1wt9Z3DP8O6W3rvwCt0REIlshg1InHImaLW0t3ObY0= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= @@ -187,6 +188,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -835,15 +837,15 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -924,8 +926,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -934,8 +936,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1044,8 +1046,8 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1061,8 +1063,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE= +google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From d871f12f367263f55e756bd7adc987dfc4becd13 Mon Sep 17 00:00:00 2001 From: Liang Deng <283304489@qq.com> Date: Mon, 19 Jun 2023 14:03:11 +0800 Subject: [PATCH 38/93] feat: provide config option for yurtadm (#1547) Signed-off-by: Liang Deng <283304489@qq.com> --- pkg/yurtadm/cmd/join/join.go | 11 +++++++++++ pkg/yurtadm/cmd/join/joindata/data.go | 1 + pkg/yurtadm/cmd/join/phases/joinnode.go | 7 ++++++- pkg/yurtadm/cmd/join/phases/prepare.go | 6 ++++-- pkg/yurtadm/constants/constants.go | 4 +++- pkg/yurtadm/util/yurthub/yurthub_test.go | 4 ++++ 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/pkg/yurtadm/cmd/join/join.go b/pkg/yurtadm/cmd/join/join.go index 05e711914ec..df7784e75d0 100644 --- a/pkg/yurtadm/cmd/join/join.go +++ b/pkg/yurtadm/cmd/join/join.go @@ -42,6 +42,7 @@ import ( ) type joinOptions struct { + cfgPath string token string nodeType string nodeName string @@ -112,6 +113,9 @@ func NewCmdJoin(in io.Reader, out io.Writer, outErr io.Writer) *cobra.Command { // addJoinConfigFlags adds join flags bound to the config to the specified flagset func addJoinConfigFlags(flagSet *flag.FlagSet, joinOptions *joinOptions) { + flagSet.StringVar( + &joinOptions.cfgPath, yurtconstants.CfgPath, "", "Path to a joinConfiguration file.", + ) flagSet.StringVar( &joinOptions.token, yurtconstants.TokenStr, "", "Use this token for both discovery-token and tls-bootstrap-token when those values are not provided.", @@ -207,6 +211,7 @@ func (nodeJoiner *nodeJoiner) Run() error { } type joinData struct { + cfgPath string joinNodeData *joindata.NodeRegistration apiServerEndpoint string token string @@ -271,6 +276,7 @@ func newJoinData(args []string, opt *joinOptions) (*joinData, error) { } data := &joinData{ + cfgPath: opt.cfgPath, apiServerEndpoint: apiServerEndpoint, token: opt.token, tlsBootstrapCfg: nil, @@ -366,6 +372,11 @@ func newJoinData(args []string, opt *joinOptions) (*joinData, error) { return data, nil } +// CfgPath returns path to a joinConfiguration file. +func (j *joinData) CfgPath() string { + return j.cfgPath +} + // ServerAddr returns the public address of kube-apiserver. func (j *joinData) ServerAddr() string { return j.apiServerEndpoint diff --git a/pkg/yurtadm/cmd/join/joindata/data.go b/pkg/yurtadm/cmd/join/joindata/data.go index 9d50ccdd570..8f5a78322d2 100644 --- a/pkg/yurtadm/cmd/join/joindata/data.go +++ b/pkg/yurtadm/cmd/join/joindata/data.go @@ -31,6 +31,7 @@ type NodeRegistration struct { } type YurtJoinData interface { + CfgPath() string ServerAddr() string JoinToken() string PauseImage() string diff --git a/pkg/yurtadm/cmd/join/phases/joinnode.go b/pkg/yurtadm/cmd/join/phases/joinnode.go index f0568ffd0fb..ad39880a2ef 100644 --- a/pkg/yurtadm/cmd/join/phases/joinnode.go +++ b/pkg/yurtadm/cmd/join/phases/joinnode.go @@ -29,7 +29,12 @@ import ( // RunJoinNode executes the node join process. func RunJoinNode(data joindata.YurtJoinData, out io.Writer, outErr io.Writer) error { - kubeadmJoinConfigFilePath := filepath.Join(constants.KubeletWorkdir, constants.KubeadmJoinConfigFileName) + var kubeadmJoinConfigFilePath string + if data.CfgPath() != "" { + kubeadmJoinConfigFilePath = data.CfgPath() + } else { + kubeadmJoinConfigFilePath = filepath.Join(constants.KubeletWorkdir, constants.KubeadmJoinConfigFileName) + } kubeadmCmd := exec.Command("kubeadm", "join", fmt.Sprintf("--config=%s", kubeadmJoinConfigFilePath)) kubeadmCmd.Stdout = out kubeadmCmd.Stderr = outErr diff --git a/pkg/yurtadm/cmd/join/phases/prepare.go b/pkg/yurtadm/cmd/join/phases/prepare.go index a4839a4a00a..66842b723a6 100644 --- a/pkg/yurtadm/cmd/join/phases/prepare.go +++ b/pkg/yurtadm/cmd/join/phases/prepare.go @@ -76,8 +76,10 @@ func RunPrepare(data joindata.YurtJoinData) error { if err := yurtadmutil.SetDiscoveryConfig(data); err != nil { return err } - if err := yurtadmutil.SetKubeadmJoinConfig(data); err != nil { - return err + if data.CfgPath() == "" { + if err := yurtadmutil.SetKubeadmJoinConfig(data); err != nil { + return err + } } return nil } diff --git a/pkg/yurtadm/constants/constants.go b/pkg/yurtadm/constants/constants.go index 49eded67302..f82b41d38ff 100644 --- a/pkg/yurtadm/constants/constants.go +++ b/pkg/yurtadm/constants/constants.go @@ -92,7 +92,9 @@ const ( Organizations = "organizations" // PauseImage flag sets the pause image for worker node. PauseImage = "pause-image" - // TokenStr flags sets both the discovery-token and the tls-bootstrap-token when those values are not provided + // CfgPath flag sets the path to a JoinConfiguration file. + CfgPath = "config" + // TokenStr flag sets both the discovery-token and the tls-bootstrap-token when those values are not provided TokenStr = "token" // TokenDiscoveryCAHash flag instruct kubeadm to validate that the root CA public key matches this hash (for token-based discovery) TokenDiscoveryCAHash = "discovery-token-ca-cert-hash" diff --git a/pkg/yurtadm/util/yurthub/yurthub_test.go b/pkg/yurtadm/util/yurthub/yurthub_test.go index 0488df46ed3..d1cb1ca476a 100644 --- a/pkg/yurtadm/util/yurthub/yurthub_test.go +++ b/pkg/yurtadm/util/yurthub/yurthub_test.go @@ -235,6 +235,10 @@ type testData struct { joinNodeData *joindata.NodeRegistration } +func (j *testData) CfgPath() string { + return "" +} + func (j *testData) ServerAddr() string { return "" } From 477b59d687b2e2948318e1d24d6eb24f44e57cc0 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Mon, 19 Jun 2023 20:16:12 +0800 Subject: [PATCH 39/93] add yurtadm to install/uninstall staticpod (#1550) --- pkg/yurtadm/cmd/cmd.go | 2 + pkg/yurtadm/cmd/join/join.go | 51 +++++++- pkg/yurtadm/cmd/join/join_test.go | 62 ++++++++++ pkg/yurtadm/cmd/join/joindata/data.go | 2 + pkg/yurtadm/cmd/join/phases/prepare.go | 7 ++ pkg/yurtadm/cmd/staticpods/install/install.go | 114 ++++++++++++++++++ pkg/yurtadm/cmd/staticpods/staticpods.go | 69 +++++++++++ .../cmd/staticpods/uninstall/uninstall.go | 110 +++++++++++++++++ pkg/yurtadm/constants/constants.go | 2 + pkg/yurtadm/util/edgenode/edgenode.go | 48 ++++++++ pkg/yurtadm/util/edgenode/edgenode_test.go | 74 ++++++++++++ pkg/yurtadm/util/kubernetes/kubernetes.go | 25 +++- pkg/yurtadm/util/yurthub/yurthub.go | 8 ++ pkg/yurtadm/util/yurthub/yurthub_test.go | 37 ++++++ 14 files changed, 607 insertions(+), 4 deletions(-) create mode 100644 pkg/yurtadm/cmd/staticpods/install/install.go create mode 100644 pkg/yurtadm/cmd/staticpods/staticpods.go create mode 100644 pkg/yurtadm/cmd/staticpods/uninstall/uninstall.go diff --git a/pkg/yurtadm/cmd/cmd.go b/pkg/yurtadm/cmd/cmd.go index 6c22e4ad99c..7c30e44c471 100644 --- a/pkg/yurtadm/cmd/cmd.go +++ b/pkg/yurtadm/cmd/cmd.go @@ -30,6 +30,7 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/join" "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/renew" "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/reset" + "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/staticpods" "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/token" ) @@ -50,6 +51,7 @@ func NewYurtadmCommand() *cobra.Command { cmds.AddCommand(token.NewCmdToken(os.Stdin, os.Stdout, os.Stderr)) cmds.AddCommand(docs.NewDocsCmd(cmds)) cmds.AddCommand(renew.NewCmdRenew(os.Stdin, os.Stdout, os.Stderr)) + cmds.AddCommand(staticpods.NewCmdStaticPods(os.Stdin, os.Stdout, os.Stderr)) klog.InitFlags(nil) // goflag.Parse() diff --git a/pkg/yurtadm/cmd/join/join.go b/pkg/yurtadm/cmd/join/join.go index df7784e75d0..ee81077a6be 100644 --- a/pkg/yurtadm/cmd/join/join.go +++ b/pkg/yurtadm/cmd/join/join.go @@ -38,6 +38,7 @@ import ( yurtconstants "github.com/openyurtio/openyurt/pkg/yurtadm/constants" "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" + "github.com/openyurtio/openyurt/pkg/yurtadm/util/yurthub" nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" ) @@ -59,6 +60,7 @@ type joinOptions struct { kubernetesResourceServer string yurthubServer string reuseCNIBin bool + staticPods string } // newJoinOptions returns a struct ready for being used for creating cmd join flags. @@ -180,6 +182,10 @@ func addJoinConfigFlags(flagSet *flag.FlagSet, joinOptions *joinOptions) { &joinOptions.reuseCNIBin, yurtconstants.ReuseCNIBin, false, "Whether to reuse local CNI binaries or to download new ones", ) + flagSet.StringVar( + &joinOptions.staticPods, yurtconstants.StaticPods, joinOptions.staticPods, + "Set the specified static pods on this node want to install", + ) } func newJoinerWithJoinData(o *joinData, in io.Reader, out io.Writer, outErr io.Writer) *nodeJoiner { @@ -230,6 +236,8 @@ type joinData struct { yurthubServer string reuseCNIBin bool namespace string + staticPodTemplateList []string + staticPodManifestList []string } // newJoinData returns a new joinData struct to be used for the execution of the kubeadm join workflow. @@ -351,6 +359,39 @@ func newJoinData(args []string, opt *joinOptions) (*joinData, error) { // add nodePool label for node by kubelet data.nodeLabels[nodepoolv1alpha1.LabelDesiredNodePool] = opt.nodePoolName } + + // check static pods has value and yurtstaticset is already exist + if len(opt.staticPods) != 0 { + // check format and split data + yssList := strings.Split(opt.staticPods, ",") + if len(yssList) < 1 { + return nil, errors.Errorf("--static-pods (%s) format is invalid, expect yss1.ns/yss1.name,yss2.ns/yss2.name", opt.staticPods) + } + + templateList := make([]string, len(yssList)) + manifestList := make([]string, len(yssList)) + for i, yss := range yssList { + info := strings.Split(yss, "/") + if len(info) != 2 { + return nil, errors.Errorf("--static-pods (%s) format is invalid, expect yss1.ns/yss1.name,yss2.ns/yss2.name", opt.staticPods) + } + + // yurthub is system static pod, can not operate + if yurthub.CheckYurtHubItself(info[0], info[1]) { + return nil, errors.Errorf("static-pods (%s) value is invalid, can not operate yurt-hub static pod", opt.staticPods) + } + + // get static pod template + manifest, staticPodTemplate, err := yurtadmutil.GetStaticPodTemplateFromConfigMap(client, info[0], util.WithConfigMapPrefix(info[1])) + if err != nil { + return nil, errors.Errorf("when --static-podsis specified, the specified yurtstaticset and configmap should be exist.") + } + templateList[i] = staticPodTemplate + manifestList[i] = manifest + } + data.staticPodTemplateList = templateList + data.staticPodManifestList = manifestList + } klog.Infof("node join data info: %#+v", *data) // get the yurthub template from the staticpod cr @@ -359,7 +400,7 @@ func newJoinData(args []string, opt *joinOptions) (*joinData, error) { yurthubYurtStaticSetName = yurtconstants.YurthubCloudYurtStaticSetName } - yurthubManifest, yurthubTemplate, err := yurtadmutil.GetYurthubTemplateFromStaticPod(client, opt.namespace, util.WithConfigMapPrefix(yurthubYurtStaticSetName)) + yurthubManifest, yurthubTemplate, err := yurtadmutil.GetStaticPodTemplateFromConfigMap(client, opt.namespace, util.WithConfigMapPrefix(yurthubYurtStaticSetName)) if err != nil { klog.Errorf("hard-code yurthub manifest will be used, because failed to get yurthub template from kube-apiserver, %v", err) yurthubManifest = yurtconstants.YurthubStaticPodManifest @@ -454,3 +495,11 @@ func (j *joinData) ReuseCNIBin() bool { func (j *joinData) Namespace() string { return j.namespace } + +func (j *joinData) StaticPodTemplateList() []string { + return j.staticPodTemplateList +} + +func (j *joinData) StaticPodManifestList() []string { + return j.staticPodManifestList +} diff --git a/pkg/yurtadm/cmd/join/join_test.go b/pkg/yurtadm/cmd/join/join_test.go index 0536d163607..29fe42ad1d5 100644 --- a/pkg/yurtadm/cmd/join/join_test.go +++ b/pkg/yurtadm/cmd/join/join_test.go @@ -650,3 +650,65 @@ func TestKubernetesResourceServer(t *testing.T) { }) } } + +func TestStaticPodTemplateList(t *testing.T) { + jd := joinData{ + staticPodTemplateList: []string{}, + } + + tests := []struct { + name string + expect []string + }{ + { + "normal", + []string{}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + t.Logf("\tTestCase: %s", tt.name) + { + get := jd.StaticPodTemplateList() + if !reflect.DeepEqual(tt.expect, get) { + t.Fatalf("\t%s\texpect %v, but get %v", failed, tt.expect, get) + } + t.Logf("\t%s\texpect %v, get %v", succeed, tt.expect, get) + } + }) + } +} + +func TestStaticPodManifestList(t *testing.T) { + jd := joinData{ + staticPodManifestList: []string{}, + } + + tests := []struct { + name string + expect []string + }{ + { + "normal", + []string{}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + t.Logf("\tTestCase: %s", tt.name) + { + get := jd.StaticPodManifestList() + if !reflect.DeepEqual(tt.expect, get) { + t.Fatalf("\t%s\texpect %v, but get %v", failed, tt.expect, get) + } + t.Logf("\t%s\texpect %v, get %v", succeed, tt.expect, get) + } + }) + } +} diff --git a/pkg/yurtadm/cmd/join/joindata/data.go b/pkg/yurtadm/cmd/join/joindata/data.go index 8f5a78322d2..80b1ad015f6 100644 --- a/pkg/yurtadm/cmd/join/joindata/data.go +++ b/pkg/yurtadm/cmd/join/joindata/data.go @@ -49,4 +49,6 @@ type YurtJoinData interface { KubernetesResourceServer() string ReuseCNIBin() bool Namespace() string + StaticPodTemplateList() []string + StaticPodManifestList() []string } diff --git a/pkg/yurtadm/cmd/join/phases/prepare.go b/pkg/yurtadm/cmd/join/phases/prepare.go index 66842b723a6..484378baaa8 100644 --- a/pkg/yurtadm/cmd/join/phases/prepare.go +++ b/pkg/yurtadm/cmd/join/phases/prepare.go @@ -24,6 +24,7 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/join/joindata" "github.com/openyurtio/openyurt/pkg/yurtadm/constants" + "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" "github.com/openyurtio/openyurt/pkg/yurtadm/util/system" "github.com/openyurtio/openyurt/pkg/yurtadm/util/yurthub" @@ -73,6 +74,12 @@ func RunPrepare(data joindata.YurtJoinData) error { if err := yurthub.AddYurthubStaticYaml(data, constants.StaticPodPath); err != nil { return err } + if len(data.StaticPodTemplateList()) != 0 { + // deploy user specified static pods + if err := edgenode.DeployStaticYaml(data.StaticPodManifestList(), data.StaticPodTemplateList(), constants.StaticPodPath); err != nil { + return err + } + } if err := yurtadmutil.SetDiscoveryConfig(data); err != nil { return err } diff --git a/pkg/yurtadm/cmd/staticpods/install/install.go b/pkg/yurtadm/cmd/staticpods/install/install.go new file mode 100644 index 00000000000..a58606dcd59 --- /dev/null +++ b/pkg/yurtadm/cmd/staticpods/install/install.go @@ -0,0 +1,114 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package install + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" + "github.com/openyurtio/openyurt/pkg/yurtadm/constants" + "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" + yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" + "github.com/openyurtio/openyurt/pkg/yurtadm/util/yurthub" +) + +type installOptions struct { + staticPods string + staticPodTemplateList []string + staticPodManifestList []string +} + +// NewCmdInstall returns "yurtadm staticpods install" command. +func NewCmdInstall() *cobra.Command { + o := &installOptions{} + + installCmd := &cobra.Command{ + Use: "install", + Short: "Install static pods for user specified.", + RunE: func(installCmd *cobra.Command, args []string) error { + if err := o.validate(); err != nil { + klog.Fatalf("validate options: %v", err) + } + + klog.Infof("Install static pods %+v", o.staticPods) + + if err := edgenode.DeployStaticYaml(o.staticPodManifestList, o.staticPodTemplateList, constants.StaticPodPath); err != nil { + return err + } + return nil + }, + } + + addInstallConfigFlags(installCmd.Flags(), o) + return installCmd +} + +func (options *installOptions) validate() error { + if len(options.staticPods) == 0 { + return fmt.Errorf("static-pods is empty") + } + + yssList := strings.Split(options.staticPods, ",") + if len(yssList) < 1 { + return errors.Errorf("static-pods (%s) format is invalid, expect yss1.ns/yss1.name,yss2.ns/yss2.name", options.staticPods) + } + + clientSet, err := yurtadmutil.GetDefaultClientSet() + if err != nil { + return err + } + + templateList := make([]string, len(yssList)) + manifestList := make([]string, len(yssList)) + for i, yss := range yssList { + info := strings.Split(yss, "/") + if len(info) != 2 { + return errors.Errorf("static-pods (%s) format is invalid, expect yss1.ns/yss1.name,yss2.ns/yss2.name", options.staticPods) + } + + // yurthub is system static pod, can not operate + if yurthub.CheckYurtHubItself(info[0], info[1]) { + return errors.Errorf("static-pods (%s) value is invalid, can not operate yurt-hub static pod", options.staticPods) + } + + // get static pod template + manifest, staticPodTemplate, err := yurtadmutil.GetStaticPodTemplateFromConfigMap(clientSet, info[0], util.WithConfigMapPrefix(info[1])) + if err != nil { + return errors.Errorf("when --static-podsis specified, the specified yurtstaticset and configmap should be exist.") + } + templateList[i] = staticPodTemplate + manifestList[i] = manifest + } + options.staticPodManifestList = manifestList + options.staticPodTemplateList = templateList + + return nil +} + +// addInstallConfigFlags adds install flags +func addInstallConfigFlags(flagSet *flag.FlagSet, installOptions *installOptions) { + flagSet.StringVar( + &installOptions.staticPods, constants.StaticPods, installOptions.staticPods, + "Set the specified static pods on this node want to install.", + ) +} diff --git a/pkg/yurtadm/cmd/staticpods/staticpods.go b/pkg/yurtadm/cmd/staticpods/staticpods.go new file mode 100644 index 00000000000..f4fda5885e6 --- /dev/null +++ b/pkg/yurtadm/cmd/staticpods/staticpods.go @@ -0,0 +1,69 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package staticpods + +import ( + "fmt" + "io" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/staticpods/install" + "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/staticpods/uninstall" + util "github.com/openyurtio/openyurt/pkg/yurtadm/util/error" +) + +// NewCmdStaticPods returns "yurtadm staticpods" command. +func NewCmdStaticPods(in io.Reader, out io.Writer, outErr io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "staticpods", + Short: "Install or uninstall static pods", + // Without this callback, if a user runs just the "staticpods" + // command without a subcommand, or with an invalid subcommand, + // cobra will print usage information, but still exit cleanly. + // We want to return an error code in these cases so that the + // user knows that their command was invalid. + Run: subCmdRun(), + } + + cmd.AddCommand(install.NewCmdInstall()) + cmd.AddCommand(uninstall.NewCmdUnInstall()) + return cmd +} + +// subCmdRun returns a function that handles a case where a subcommand must be specified +// Without this callback, if a user runs just the command without a subcommand, +// or with an invalid subcommand, cobra will print usage information, but still exit cleanly. +func subCmdRun() func(c *cobra.Command, args []string) { + return func(c *cobra.Command, args []string) { + if len(args) > 0 { + util.CheckErr(usageErrorf(c, "invalid subcommand %q", strings.Join(args, " "))) + } + err := c.Help() + if err != nil { + return + } + util.CheckErr(util.ErrExit) + } +} + +func usageErrorf(c *cobra.Command, format string, args ...interface{}) error { + msg := fmt.Sprintf(format, args...) + return errors.Errorf("%s\nSee '%s -h' for help and examples", msg, c.CommandPath()) +} diff --git a/pkg/yurtadm/cmd/staticpods/uninstall/uninstall.go b/pkg/yurtadm/cmd/staticpods/uninstall/uninstall.go new file mode 100644 index 00000000000..97a142b2c35 --- /dev/null +++ b/pkg/yurtadm/cmd/staticpods/uninstall/uninstall.go @@ -0,0 +1,110 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package uninstall + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" + "github.com/openyurtio/openyurt/pkg/yurtadm/constants" + "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" + yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" + "github.com/openyurtio/openyurt/pkg/yurtadm/util/yurthub" +) + +type uninstallOptions struct { + staticPods string + staticPodManifestList []string +} + +// NewCmdUnInstall returns "yurtadm staticpods uninstall" command. +func NewCmdUnInstall() *cobra.Command { + o := &uninstallOptions{} + + uninstallCmd := &cobra.Command{ + Use: "uninstall", + Short: "UnInstall static pods for user specified.", + RunE: func(uninstallCmd *cobra.Command, args []string) error { + if err := o.validate(); err != nil { + klog.Fatalf("validate options: %v", err) + } + + klog.Infof("UnInstall static pods %+v", o.staticPods) + + if err := edgenode.RemoveStaticYaml(o.staticPodManifestList, constants.StaticPodPath); err != nil { + return err + } + return nil + }, + } + + addUnInstallConfigFlags(uninstallCmd.Flags(), o) + return uninstallCmd +} + +func (options *uninstallOptions) validate() error { + if len(options.staticPods) == 0 { + return fmt.Errorf("static-pods is empty") + } + + yssList := strings.Split(options.staticPods, ",") + if len(yssList) < 1 { + return errors.Errorf("static-pods (%s) format is invalid, expect yss1.ns/yss1.name,yss2.ns/yss2.name", options.staticPods) + } + + clientSet, err := yurtadmutil.GetDefaultClientSet() + if err != nil { + return err + } + + manifestList := make([]string, len(yssList)) + for i, yss := range yssList { + info := strings.Split(yss, "/") + if len(info) != 2 { + return errors.Errorf("static-pods (%s) format is invalid, expect yss1.ns/yss1.name,yss2.ns/yss2.name", options.staticPods) + } + + // yurthub is system static pod, can not operate + if yurthub.CheckYurtHubItself(info[0], info[1]) { + return errors.Errorf("static-pods (%s) value is invalid, can not operate yurt-hub static pod", options.staticPods) + } + + // get static pod template + manifest, _, err := yurtadmutil.GetStaticPodTemplateFromConfigMap(clientSet, info[0], util.WithConfigMapPrefix(info[1])) + if err != nil { + return errors.Errorf("when --static-podsis specified, the specified yurtstaticset and configmap should be exist.") + } + manifestList[i] = manifest + } + options.staticPodManifestList = manifestList + + return nil +} + +// addUnInstallConfigFlags adds uninstall flags +func addUnInstallConfigFlags(flagSet *flag.FlagSet, uninstallOptions *uninstallOptions) { + flagSet.StringVar( + &uninstallOptions.staticPods, constants.StaticPods, uninstallOptions.staticPods, + "Set the specified static pods on this node want to uninstall.", + ) +} diff --git a/pkg/yurtadm/constants/constants.go b/pkg/yurtadm/constants/constants.go index f82b41d38ff..77f7e63485e 100644 --- a/pkg/yurtadm/constants/constants.go +++ b/pkg/yurtadm/constants/constants.go @@ -110,6 +110,8 @@ const ( ServerAddr = "server-addr" // ReuseCNIBin flag sets whether to reuse local CNI binaries or not. ReuseCNIBin = "reuse-cni-bin" + // StaticPods flag set the specified static pods on this node want to install + StaticPods = "static-pods" DefaultServerAddr = "https://127.0.0.1:6443" ServerHealthzServer = "127.0.0.1:10267" diff --git a/pkg/yurtadm/util/edgenode/edgenode.go b/pkg/yurtadm/util/edgenode/edgenode.go index e213e0ed553..26304306913 100644 --- a/pkg/yurtadm/util/edgenode/edgenode.go +++ b/pkg/yurtadm/util/edgenode/edgenode.go @@ -20,11 +20,13 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "regexp" "strings" "k8s.io/klog/v2" + "github.com/openyurtio/openyurt/pkg/node-servant/static-pod-upgrade/util" "github.com/openyurtio/openyurt/pkg/yurtadm/constants" ) @@ -176,3 +178,49 @@ func Exec(cmd *exec.Cmd) error { func GetPodManifestPath() string { return constants.StaticPodPath // /etc/kubernetes/manifests } + +func DeployStaticYaml(manifestList, templateList []string, podManifestPath string) error { + klog.Info("Deploying user edge static yaml") + if _, err := os.Stat(podManifestPath); err != nil { + if os.IsNotExist(err) { + err = os.MkdirAll(podManifestPath, os.ModePerm) + if err != nil { + return err + } + } else { + klog.Errorf("Describe dir %s fail: %v", podManifestPath, err) + return err + } + } + + for i, template := range templateList { + manifestFile := filepath.Join(podManifestPath, util.WithYamlSuffix(manifestList[i])) + klog.Infof("static pod template: %s\n%s", manifestFile, template) + if err := os.WriteFile(manifestFile, []byte(template), 0600); err != nil { + return err + } + } + + klog.Info("Deploy user edge static yaml is ok") + return nil +} + +func RemoveStaticYaml(manifestList []string, podManifestPath string) error { + klog.Info("Removing user edge static yaml") + if _, err := os.Stat(podManifestPath); err != nil { + if os.IsNotExist(err) { + klog.Errorf("Describe dir %s fail: %v", podManifestPath, err) + return err + } + } + + for _, manifest := range manifestList { + manifestFile := filepath.Join(podManifestPath, util.WithYamlSuffix(manifest)) + if err := os.Remove(manifestFile); err != nil { + return err + } + } + + klog.Info("Remove user edge static yaml is ok") + return nil +} diff --git a/pkg/yurtadm/util/edgenode/edgenode_test.go b/pkg/yurtadm/util/edgenode/edgenode_test.go index 2719611d3c4..878b1bf5b23 100644 --- a/pkg/yurtadm/util/edgenode/edgenode_test.go +++ b/pkg/yurtadm/util/edgenode/edgenode_test.go @@ -95,3 +95,77 @@ func Test_GetHostName(t *testing.T) { }) } } + +func TestDeployStaticYaml(t *testing.T) { + tests := []struct { + name string + manifestList []string + templateList []string + podManifestPath string + wantErr bool + }{ + { + name: "test1", + manifestList: []string{"nginx"}, + templateList: []string{"xxxxxx"}, + podManifestPath: "/tmp", + wantErr: false, + }, + { + name: "test2", + manifestList: []string{"nginx"}, + templateList: []string{"xxxxxx"}, + podManifestPath: "/etc/kubernetes/?", + wantErr: true, + }, + { + name: "test3", + manifestList: []string{"nginx"}, + templateList: []string{"xxxxxx"}, + podManifestPath: "/root", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := DeployStaticYaml(tt.manifestList, tt.templateList, tt.podManifestPath); (err != nil) != tt.wantErr { + t.Errorf("DeployStaticYaml() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestRemoveStaticYaml(t *testing.T) { + tests := []struct { + name string + manifestList []string + podManifestPath string + wantErr bool + }{ + { + name: "test1", + manifestList: []string{"nginx"}, + podManifestPath: "/tmp", + wantErr: false, + }, + { + name: "test2", + manifestList: []string{"nginx"}, + podManifestPath: "/etc/kubernetes/?", + wantErr: true, + }, + { + name: "test3", + manifestList: []string{"nginx"}, + podManifestPath: "/root", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := RemoveStaticYaml(tt.manifestList, tt.podManifestPath); (err != nil) != tt.wantErr { + t.Errorf("RemoveStaticYaml() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/yurtadm/util/kubernetes/kubernetes.go b/pkg/yurtadm/util/kubernetes/kubernetes.go index 31d5de8bf82..6e0710b44ca 100644 --- a/pkg/yurtadm/util/kubernetes/kubernetes.go +++ b/pkg/yurtadm/util/kubernetes/kubernetes.go @@ -35,6 +35,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/klog/v2" @@ -504,14 +505,14 @@ func CheckKubeletStatus() error { return nil } -// GetYurthubTemplateFromStaticPod get yurthub template from static pod -func GetYurthubTemplateFromStaticPod(client kubernetes.Interface, namespace, name string) (string, string, error) { +// GetStaticPodTemplateFromConfigMap get static pod template from configmap +func GetStaticPodTemplateFromConfigMap(client kubernetes.Interface, namespace, name string) (string, string, error) { configMap, err := apiclient.GetConfigMapWithRetry( client, namespace, name) if err != nil { - return "", "", pkgerrors.Wrap(err, "failed to get configmap of yurthub yurtstaticset") + return "", "", pkgerrors.Errorf("failed to get configmap of %s/%s yurtstaticset, err: %+v", namespace, name, err) } if len(configMap.Data) == 1 { @@ -522,3 +523,21 @@ func GetYurthubTemplateFromStaticPod(client kubernetes.Interface, namespace, nam return "", "", fmt.Errorf("invalid manifest in configmap %s", name) } + +// GetDefaultClientSet return client set created by /etc/kubernetes/kubelet.conf +func GetDefaultClientSet() (*kubernetes.Clientset, error) { + kubeConfig := filepath.Join(constants.KubeletConfigureDir, constants.KubeletKubeConfigFileName) + if _, err := os.Stat(kubeConfig); err != nil && os.IsNotExist(err) { + return nil, err + } + + cfg, err := clientcmd.BuildConfigFromFlags("", kubeConfig) + if err != nil { + return nil, fmt.Errorf("fail to create the clientset based on %s: %w", kubeConfig, err) + } + cliSet, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + return cliSet, nil +} diff --git a/pkg/yurtadm/util/yurthub/yurthub.go b/pkg/yurtadm/util/yurthub/yurthub.go index 8dacfc4fd76..c1ce55b8e8b 100644 --- a/pkg/yurtadm/util/yurthub/yurthub.go +++ b/pkg/yurtadm/util/yurthub/yurthub.go @@ -203,3 +203,11 @@ func useRealServerAddr(yurthubTemplate string, kubernetesServerAddrs string) (st } return buffer.String(), nil } + +func CheckYurtHubItself(ns, name string) bool { + if ns == constants.YurthubNamespace && + (name == constants.YurthubYurtStaticSetName || name == constants.YurthubCloudYurtStaticSetName) { + return true + } + return false +} diff --git a/pkg/yurtadm/util/yurthub/yurthub_test.go b/pkg/yurtadm/util/yurthub/yurthub_test.go index d1cb1ca476a..aa3cf3bd87d 100644 --- a/pkg/yurtadm/util/yurthub/yurthub_test.go +++ b/pkg/yurtadm/util/yurthub/yurthub_test.go @@ -307,6 +307,14 @@ func (j *testData) Namespace() string { return "" } +func (j *testData) StaticPodTemplateList() []string { + return nil +} + +func (j *testData) StaticPodManifestList() []string { + return nil +} + func TestAddYurthubStaticYaml(t *testing.T) { xdata := testData{ joinNodeData: &joindata.NodeRegistration{ @@ -338,3 +346,32 @@ func TestAddYurthubStaticYaml(t *testing.T) { }) } } + +func TestCheckYurtHubItself(t *testing.T) { + tests := []struct { + testName string + ns string + name string + want bool + }{ + { + testName: "test1", + ns: "kube-system", + name: "yurt-hub", + want: true, + }, + { + testName: "test2", + ns: "cattle-system", + name: "yurt-hub", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CheckYurtHubItself(tt.ns, tt.name); got != tt.want { + t.Errorf("CheckYurtHubItself() = %v, want %v", got, tt.want) + } + }) + } +} From c730e6445dd9fbeba9e106a1f0bd367a8fb0ea37 Mon Sep 17 00:00:00 2001 From: rambohe Date: Wed, 21 Jun 2023 14:36:13 +0800 Subject: [PATCH 40/93] update chart version from v1.3.0 to v1.3.2 (#1563) --- charts/yurt-coordinator/Chart.yaml | 4 ++-- charts/yurt-manager/Chart.yaml | 4 ++-- charts/yurt-manager/values.yaml | 2 +- charts/yurthub/Chart.yaml | 4 ++-- charts/yurthub/values.yaml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/yurt-coordinator/Chart.yaml b/charts/yurt-coordinator/Chart.yaml index 8676106d32f..66ed1ed4043 100644 --- a/charts/yurt-coordinator/Chart.yaml +++ b/charts/yurt-coordinator/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.1 +version: 1.3.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.3.0" +appVersion: "1.3.2" diff --git a/charts/yurt-manager/Chart.yaml b/charts/yurt-manager/Chart.yaml index 74238dda7b3..f4fac8040ca 100644 --- a/charts/yurt-manager/Chart.yaml +++ b/charts/yurt-manager/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.1 +version: 1.3.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.3.0" +appVersion: "1.3.2" diff --git a/charts/yurt-manager/values.yaml b/charts/yurt-manager/values.yaml index 9ea92685bde..b8036028028 100644 --- a/charts/yurt-manager/values.yaml +++ b/charts/yurt-manager/values.yaml @@ -13,7 +13,7 @@ nameOverride: "" image: registry: openyurt repository: yurt-manager - tag: v1.3.0 + tag: v1.3.2 ports: metrics: 10271 diff --git a/charts/yurthub/Chart.yaml b/charts/yurthub/Chart.yaml index b651e51caed..e178cd615c8 100644 --- a/charts/yurthub/Chart.yaml +++ b/charts/yurthub/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.1 +version: 1.3.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.3.0" +appVersion: "1.3.2" diff --git a/charts/yurthub/values.yaml b/charts/yurthub/values.yaml index c6d81191837..990d5965fd0 100644 --- a/charts/yurthub/values.yaml +++ b/charts/yurthub/values.yaml @@ -14,4 +14,4 @@ organizations: "" image: registry: openyurt repository: yurthub - tag: v1.3.0 \ No newline at end of file + tag: v1.3.2 \ No newline at end of file From e7bf0a76c21bea4d9e90afb2e7ad3ddbfe00488b Mon Sep 17 00:00:00 2001 From: Haiyu Zuo <958474674@qq.com> Date: Sat, 24 Jun 2023 17:07:13 +0800 Subject: [PATCH 41/93] feat:improve service topology controller by using client.Client instead of kubernetes.Interface (#1565) Signed-off-by: zhy76 <958474674@qq.com> --- .../adapter/endpoints_adapter.go | 20 ++++++++++++------- .../adapter/endpoints_adapter_test.go | 12 +++++------ .../adapter/endpointslicev1_adapter.go | 20 ++++++++++++------- .../adapter/endpointslicev1_adapter_test.go | 12 +++++------ .../adapter/endpointslicev1beta1_adapter.go | 20 ++++++++++++------- .../endpointslicev1beta1_adapter_test.go | 12 +++++------ .../endpoints/endpoints_controller.go | 13 +----------- .../endpointslice/endpointslice_controller.go | 17 ++-------------- 8 files changed, 57 insertions(+), 69 deletions(-) diff --git a/pkg/controller/servicetopology/adapter/endpoints_adapter.go b/pkg/controller/servicetopology/adapter/endpoints_adapter.go index 7a7c7d86d77..958289b99ec 100644 --- a/pkg/controller/servicetopology/adapter/endpoints_adapter.go +++ b/pkg/controller/servicetopology/adapter/endpoints_adapter.go @@ -23,20 +23,17 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client" ) -func NewEndpointsAdapter(kubeClient kubernetes.Interface, client client.Client) Adapter { +func NewEndpointsAdapter(client client.Client) Adapter { return &endpoints{ - kubeClient: kubeClient, - client: client, + client: client, } } type endpoints struct { - kubeClient kubernetes.Interface - client client.Client + client client.Client } func (s *endpoints) GetEnqueueKeysBySvc(svc *corev1.Service) []string { @@ -46,6 +43,15 @@ func (s *endpoints) GetEnqueueKeysBySvc(svc *corev1.Service) []string { func (s *endpoints) UpdateTriggerAnnotations(namespace, name string) error { patch := getUpdateTriggerPatch() - _, err := s.kubeClient.CoreV1().Endpoints(namespace).Patch(context.Background(), name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) + err := s.client.Patch( + context.Background(), + &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }, + client.RawPatch(types.StrategicMergePatchType, patch), &client.PatchOptions{}, + ) return err } diff --git a/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go b/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go index 1f4788d2e60..a86adec52df 100644 --- a/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go +++ b/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go @@ -24,7 +24,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/cache" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -32,18 +32,17 @@ import ( func TestEndpointAdapterUpdateTriggerAnnotations(t *testing.T) { ep := getEndpoints("default", "svc1", "node1") - kubeClient := fake.NewSimpleClientset(ep) stopper := make(chan struct{}) defer close(stopper) c := fakeclient.NewClientBuilder().WithObjects(ep).Build() - adapter := NewEndpointsAdapter(kubeClient, c) + adapter := NewEndpointsAdapter(c) err := adapter.UpdateTriggerAnnotations(ep.Namespace, ep.Name) if err != nil { t.Errorf("update endpoints trigger annotations failed") } - - newEp, err := kubeClient.CoreV1().Endpoints(ep.Namespace).Get(context.TODO(), ep.Name, metav1.GetOptions{}) + newEp := &corev1.Endpoints{} + err = c.Get(context.TODO(), types.NamespacedName{Namespace: ep.Namespace, Name: ep.Name}, newEp) if err != nil || ep.Annotations["openyurt.io/update-trigger"] == newEp.Annotations["openyurt.io/update-trigger"] { t.Errorf("update endpoints trigger annotations failed") } @@ -60,11 +59,10 @@ func TestEndpointAdapterGetEnqueueKeysBySvc(t *testing.T) { ep := getEndpoints("default", "svc1", "node1") - kubeClient := fake.NewSimpleClientset(ep) stopper := make(chan struct{}) defer close(stopper) c := fakeclient.NewClientBuilder().WithObjects(ep).Build() - adapter := NewEndpointsAdapter(kubeClient, c) + adapter := NewEndpointsAdapter(c) keys := adapter.GetEnqueueKeysBySvc(svc) if !reflect.DeepEqual(keys, expectResult) { diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go b/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go index 72814822e85..73be71b2a39 100644 --- a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go +++ b/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go @@ -24,21 +24,18 @@ import ( discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" ) -func NewEndpointsV1Adapter(kubeClient kubernetes.Interface, client client.Client) Adapter { +func NewEndpointsV1Adapter(client client.Client) Adapter { return &endpointslicev1{ - kubeClient: kubeClient, - client: client, + client: client, } } type endpointslicev1 struct { - kubeClient kubernetes.Interface - client client.Client + client client.Client } func (s *endpointslicev1) GetEnqueueKeysBySvc(svc *corev1.Service) []string { @@ -58,6 +55,15 @@ func (s *endpointslicev1) GetEnqueueKeysBySvc(svc *corev1.Service) []string { func (s *endpointslicev1) UpdateTriggerAnnotations(namespace, name string) error { patch := getUpdateTriggerPatch() - _, err := s.kubeClient.DiscoveryV1().EndpointSlices(namespace).Patch(context.Background(), name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) + err := s.client.Patch( + context.Background(), + &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }, + client.RawPatch(types.StrategicMergePatchType, patch), &client.PatchOptions{}, + ) return err } diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go b/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go index d8719994333..d651762f66b 100644 --- a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go +++ b/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go @@ -26,7 +26,7 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" + "k8s.io/apimachinery/pkg/types" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -35,17 +35,16 @@ func TestEndpointSliceV1AdapterUpdateTriggerAnnotations(t *testing.T) { svcNamespace := "default" epSlice := getEndpointSlice(svcNamespace, svcName, "node1") - kubeClient := fake.NewSimpleClientset(epSlice) c := fakeclient.NewClientBuilder().WithObjects(epSlice).Build() stopper := make(chan struct{}) defer close(stopper) - adapter := NewEndpointsV1Adapter(kubeClient, c) + adapter := NewEndpointsV1Adapter(c) err := adapter.UpdateTriggerAnnotations(epSlice.Namespace, epSlice.Name) if err != nil { t.Errorf("update endpointsSlice trigger annotations failed") } - - newEpSlice, err := kubeClient.DiscoveryV1().EndpointSlices(epSlice.Namespace).Get(context.TODO(), epSlice.Name, metav1.GetOptions{}) + newEpSlice := &discoveryv1.EndpointSlice{} + err = c.Get(context.TODO(), types.NamespacedName{Namespace: epSlice.Namespace, Name: epSlice.Name}, newEpSlice) if err != nil || epSlice.Annotations["openyurt.io/update-trigger"] == newEpSlice.Annotations["openyurt.io/update-trigger"] { t.Errorf("update endpoints trigger annotations failed") } @@ -65,9 +64,8 @@ func TestEndpointSliceV1AdapterGetEnqueueKeysBySvc(t *testing.T) { stopper := make(chan struct{}) defer close(stopper) - kubeClient := fake.NewSimpleClientset(epSlice) c := fakeclient.NewClientBuilder().WithObjects(epSlice).Build() - adapter := NewEndpointsV1Adapter(kubeClient, c) + adapter := NewEndpointsV1Adapter(c) keys := adapter.GetEnqueueKeysBySvc(svc) if !reflect.DeepEqual(keys, expectResult) { diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go b/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go index 5c35ea7f95f..a1867f2a932 100644 --- a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go +++ b/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go @@ -24,21 +24,18 @@ import ( discoveryv1beta1 "k8s.io/api/discovery/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" ) -func NewEndpointsV1Beta1Adapter(kubeClient kubernetes.Interface, client client.Client) Adapter { +func NewEndpointsV1Beta1Adapter(client client.Client) Adapter { return &endpointslicev1beta1{ - kubeClient: kubeClient, - client: client, + client: client, } } type endpointslicev1beta1 struct { - kubeClient kubernetes.Interface - client client.Client + client client.Client } func (s *endpointslicev1beta1) GetEnqueueKeysBySvc(svc *corev1.Service) []string { @@ -58,6 +55,15 @@ func (s *endpointslicev1beta1) GetEnqueueKeysBySvc(svc *corev1.Service) []string func (s *endpointslicev1beta1) UpdateTriggerAnnotations(namespace, name string) error { patch := getUpdateTriggerPatch() - _, err := s.kubeClient.DiscoveryV1beta1().EndpointSlices(namespace).Patch(context.Background(), name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}) + err := s.client.Patch( + context.Background(), + &discoveryv1beta1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }, + client.RawPatch(types.StrategicMergePatchType, patch), &client.PatchOptions{}, + ) return err } diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go b/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go index a9a874179b4..13c41ba8297 100644 --- a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go +++ b/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go @@ -26,7 +26,7 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" + "k8s.io/apimachinery/pkg/types" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -35,17 +35,16 @@ func TestEndpointSliceV1Beta1AdapterUpdateTriggerAnnotations(t *testing.T) { svcNamespace := "default" epSlice := getV1Beta1EndpointSlice(svcNamespace, svcName, "node1") - kubeClient := fake.NewSimpleClientset(epSlice) c := fakeclient.NewClientBuilder().WithObjects(epSlice).Build() stopper := make(chan struct{}) defer close(stopper) - adapter := NewEndpointsV1Beta1Adapter(kubeClient, c) + adapter := NewEndpointsV1Beta1Adapter(c) err := adapter.UpdateTriggerAnnotations(epSlice.Namespace, epSlice.Name) if err != nil { t.Errorf("update endpointsSlice trigger annotations failed") } - - newEpSlice, err := kubeClient.DiscoveryV1beta1().EndpointSlices(epSlice.Namespace).Get(context.TODO(), epSlice.Name, metav1.GetOptions{}) + newEpSlice := &discoveryv1beta1.EndpointSlice{} + err = c.Get(context.TODO(), types.NamespacedName{Namespace: epSlice.Namespace, Name: epSlice.Name}, newEpSlice) if err != nil || epSlice.Annotations["openyurt.io/update-trigger"] == newEpSlice.Annotations["openyurt.io/update-trigger"] { t.Errorf("update endpoints trigger annotations failed") } @@ -65,9 +64,8 @@ func TestEndpointSliceV1Beta1AdapterGetEnqueueKeysBySvc(t *testing.T) { stopper := make(chan struct{}) defer close(stopper) - kubeClient := fake.NewSimpleClientset(epSlice) c := fakeclient.NewClientBuilder().WithObjects(epSlice).Build() - adapter := NewEndpointsV1Beta1Adapter(kubeClient, c) + adapter := NewEndpointsV1Beta1Adapter(c) keys := adapter.GetEnqueueKeysBySvc(svc) if !reflect.DeepEqual(keys, expectResult) { diff --git a/pkg/controller/servicetopology/endpoints/endpoints_controller.go b/pkg/controller/servicetopology/endpoints/endpoints_controller.go index 228c6cc0c9a..ed1c15ad370 100644 --- a/pkg/controller/servicetopology/endpoints/endpoints_controller.go +++ b/pkg/controller/servicetopology/endpoints/endpoints_controller.go @@ -22,8 +22,6 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -78,16 +76,7 @@ func newReconciler(_ *appconfig.CompletedConfig, mgr manager.Manager) reconcile. func (r *ReconcileServicetopologyEndpoints) InjectClient(c client.Client) error { r.Client = c - return nil -} - -func (r *ReconcileServicetopologyEndpoints) InjectConfig(cfg *rest.Config) error { - c, err := kubernetes.NewForConfig(cfg) - if err != nil { - klog.Errorf(Format("failed to create kube client, %v", err)) - return err - } - r.endpointsAdapter = adapter.NewEndpointsAdapter(c, r.Client) + r.endpointsAdapter = adapter.NewEndpointsAdapter(c) return nil } diff --git a/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go b/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go index 41c91cc912b..335b2ccbbd8 100644 --- a/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go +++ b/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go @@ -25,8 +25,6 @@ import ( discoveryv1 "k8s.io/api/discovery/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -64,9 +62,9 @@ func Add(_ *appconfig.CompletedConfig, mgr manager.Manager) error { } if r.isSupportEndpointslicev1 { - r.endpointsliceAdapter = adapter.NewEndpointsV1Adapter(r.kubeClient, r.Client) + r.endpointsliceAdapter = adapter.NewEndpointsV1Adapter(r.Client) } else { - r.endpointsliceAdapter = adapter.NewEndpointsV1Beta1Adapter(r.kubeClient, r.Client) + r.endpointsliceAdapter = adapter.NewEndpointsV1Beta1Adapter(r.Client) } // Watch for changes to Service @@ -85,21 +83,10 @@ var _ reconcile.Reconciler = &ReconcileServiceTopologyEndpointSlice{} // ReconcileServiceTopologyEndpointSlice reconciles a Example object type ReconcileServiceTopologyEndpointSlice struct { client.Client - kubeClient kubernetes.Interface endpointsliceAdapter adapter.Adapter isSupportEndpointslicev1 bool } -func (r *ReconcileServiceTopologyEndpointSlice) InjectConfig(cfg *rest.Config) error { - c, err := kubernetes.NewForConfig(cfg) - if err != nil { - klog.Errorf(Format("failed to create kube client, %v", err)) - return err - } - r.kubeClient = c - return nil -} - func (r *ReconcileServiceTopologyEndpointSlice) InjectMapper(mapper meta.RESTMapper) error { if gvk, err := mapper.KindFor(v1EndpointSliceGVR); err != nil { klog.Errorf("v1.EndpointSlice is not supported, %v", err) From 5ca7a5b0bcc90f67e033dee1008b79bb3ba74bff Mon Sep 17 00:00:00 2001 From: Zhen Zhao <70508195+JameKeal@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:18:14 +0800 Subject: [PATCH 42/93] improve pod binding controller to use client.Client (#1485) * improve pod binding controller to use client.Client * fix lint and add unittest --- .../podbinding/podbinding_controller.go | 115 +++-- .../podbinding/podbinding_controller_test.go | 429 ++++++++++++++++++ 2 files changed, 504 insertions(+), 40 deletions(-) create mode 100644 pkg/controller/yurtcoordinator/podbinding/podbinding_controller_test.go diff --git a/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go b/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go index 64a702e0650..02cfd571e07 100644 --- a/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go +++ b/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go @@ -19,11 +19,11 @@ package podbinding import ( "context" "flag" + "fmt" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" + "k8s.io/apimachinery/pkg/fields" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -46,7 +46,9 @@ const ( ) var ( - concurrentReconciles = 5 + controllerKind = appsv1.SchemeGroupVersion.WithKind("Node") + concurrentReconciles = 5 + defaultTolerationSeconds = 300 notReadyToleration = corev1.Toleration{ Key: corev1.TaintNodeNotReady, @@ -59,49 +61,89 @@ var ( Operator: corev1.TolerationOpExists, Effect: corev1.TaintEffectNoExecute, } - defaultTolerationSeconds = 300 ) +func Format(format string, args ...interface{}) string { + s := fmt.Sprintf(format, args...) + return fmt.Sprintf("%s: %s", ControllerName, s) +} + type ReconcilePodBinding struct { client.Client - podBindingClient kubernetes.Interface } // Add creates a PodBingding controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. -func Add(_ *appconfig.CompletedConfig, mgr manager.Manager) error { - r := &ReconcilePodBinding{} +func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { + klog.Infof(Format("podbinding-controller add controller %s", controllerKind.String())) + return add(mgr, newReconciler(c, mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(_ *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { + return &ReconcilePodBinding{} +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { c, err := controller.New(ControllerName, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, }) if err != nil { return err } + err = c.Watch(&source.Kind{Type: &corev1.Node{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + klog.V(4).Info(Format("registering the field indexers of podbinding controller")) + err = mgr.GetFieldIndexer().IndexField(context.TODO(), &corev1.Pod{}, "spec.nodeName", func(rawObj client.Object) []string { + pod, ok := rawObj.(*corev1.Pod) + if ok { + return []string{pod.Spec.NodeName} + } + return []string{} + }) + if err != nil { + klog.Errorf(Format("failed to register field indexers for podbinding controller, %v", err)) + } return err } +func (r *ReconcilePodBinding) InjectClient(c client.Client) error { + r.Client = c + return nil +} + // Reconcile reads that state of Node in cluster and makes changes if node autonomy state has been changed func (r *ReconcilePodBinding) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { var err error node := &corev1.Node{} if err = r.Get(ctx, req.NamespacedName, node); err != nil { - klog.V(4).Infof("node not found for %q\n", req.NamespacedName) + klog.V(4).Infof(Format("node not found for %q\n", req.NamespacedName)) return reconcile.Result{}, client.IgnoreNotFound(err) } - klog.V(4).Infof("node request: %s\n", node.Name) - r.processNode(ctx, node) + klog.V(4).Infof(Format("node request: %s\n", node.Name)) + + if err := r.processNode(node); err != nil { + return reconcile.Result{}, err + } return reconcile.Result{}, nil } -func (r *ReconcilePodBinding) processNode(ctx context.Context, node *corev1.Node) { +func (r *ReconcilePodBinding) processNode(node *corev1.Node) error { // if node has autonomy annotation, we need to see if pods on this node except DaemonSet/Static ones need a treat - pods := r.getPodsAssignedToNode(ctx, node.Name) + pods, err := r.getPodsAssignedToNode(node.Name) + if err != nil { + return err + } for i := range pods { pod := &pods[i] - klog.V(5).Infof("pod %d on node %s: %s\n", i, node.Name, pod.Name) + klog.V(5).Infof(Format("pod %d on node %s: %s", i, node.Name, pod.Name)) // skip DaemonSet pods and static pod if isDaemonSetPodOrStaticPod(pod) { continue @@ -115,24 +157,32 @@ func (r *ReconcilePodBinding) processNode(ctx context.Context, node *corev1.Node // pod binding takes precedence against node autonomy if isPodBoundenToNode(node) { if err := r.configureTolerationForPod(pod, nil); err != nil { - klog.Errorf("failed to configure toleration of pod, %v", err) + klog.Errorf(Format("failed to configure toleration of pod, %v", err)) } } else { tolerationSeconds := int64(defaultTolerationSeconds) if err := r.configureTolerationForPod(pod, &tolerationSeconds); err != nil { - klog.Errorf("failed to configure toleration of pod, %v", err) + klog.Errorf(Format("failed to configure toleration of pod, %v", err)) } } } + return nil } -func (r *ReconcilePodBinding) getPodsAssignedToNode(ctx context.Context, name string) []corev1.Pod { - pods, err := r.podBindingClient.CoreV1().Pods("").List(ctx, metav1.ListOptions{FieldSelector: "spec.nodeName=" + name}) +func (r *ReconcilePodBinding) getPodsAssignedToNode(name string) ([]corev1.Pod, error) { + listOptions := &client.ListOptions{ + FieldSelector: fields.SelectorFromSet(fields.Set{ + "spec.nodeName": name, + }), + } + + podList := &corev1.PodList{} + err := r.List(context.TODO(), podList, listOptions) if err != nil { - klog.Errorf("failed to get podList for node(%s), %v", name, err) - return nil + klog.Errorf(Format("failed to get podList for node(%s), %v", name, err)) + return nil, err } - return pods.Items + return podList.Items, nil } func (r *ReconcilePodBinding) configureTolerationForPod(pod *corev1.Pod, tolerationSeconds *int64) error { @@ -144,13 +194,13 @@ func (r *ReconcilePodBinding) configureTolerationForPod(pod *corev1.Pod, tolerat if toleratesNodeNotReady || toleratesNodeUnreachable { if tolerationSeconds == nil { - klog.V(4).Infof("pod(%s/%s) => toleratesNodeNotReady=%v, toleratesNodeUnreachable=%v, tolerationSeconds=0", pod.Namespace, pod.Name, toleratesNodeNotReady, toleratesNodeUnreachable) + klog.V(4).Infof(Format("pod(%s/%s) => toleratesNodeNotReady=%v, toleratesNodeUnreachable=%v, tolerationSeconds=0", pod.Namespace, pod.Name, toleratesNodeNotReady, toleratesNodeUnreachable)) } else { - klog.V(4).Infof("pod(%s/%s) => toleratesNodeNotReady=%v, toleratesNodeUnreachable=%v, tolerationSeconds=%d", pod.Namespace, pod.Name, toleratesNodeNotReady, toleratesNodeUnreachable, *tolerationSeconds) + klog.V(4).Infof(Format("pod(%s/%s) => toleratesNodeNotReady=%v, toleratesNodeUnreachable=%v, tolerationSeconds=%d", pod.Namespace, pod.Name, toleratesNodeNotReady, toleratesNodeUnreachable, *tolerationSeconds)) } - _, err := r.podBindingClient.CoreV1().Pods(pod.Namespace).Update(context.TODO(), pod, metav1.UpdateOptions{}) + err := r.Update(context.TODO(), pod, &client.UpdateOptions{}) if err != nil { - klog.Errorf("failed to update toleration of pod(%s/%s), %v", pod.Namespace, pod.Name, err) + klog.Errorf(Format("failed to update toleration of pod(%s/%s), %v", pod.Namespace, pod.Name, err)) return err } } @@ -158,21 +208,6 @@ func (r *ReconcilePodBinding) configureTolerationForPod(pod *corev1.Pod, tolerat return nil } -func (r *ReconcilePodBinding) InjectClient(c client.Client) error { - r.Client = c - return nil -} - -func (r *ReconcilePodBinding) InjectConfig(cfg *rest.Config) error { - clientSet, err := kubernetes.NewForConfig(cfg) - if err != nil { - klog.Errorf("failed to create kube client, %v", err) - return err - } - r.podBindingClient = clientSet - return nil -} - func isPodBoundenToNode(node *corev1.Node) bool { if node.Annotations != nil && (node.Annotations[projectinfo.GetAutonomyAnnotation()] == "true" || diff --git a/pkg/controller/yurtcoordinator/podbinding/podbinding_controller_test.go b/pkg/controller/yurtcoordinator/podbinding/podbinding_controller_test.go new file mode 100644 index 00000000000..fe08e7bfc42 --- /dev/null +++ b/pkg/controller/yurtcoordinator/podbinding/podbinding_controller_test.go @@ -0,0 +1,429 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package podbinding + +import ( + "context" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" +) + +var ( + TestNodesName = []string{"node1", "node2", "node3", "node4"} + TestPodsName = []string{"pod1", "pod2", "pod3", "pod4"} +) + +func prepareNodes() []client.Object { + nodes := []client.Object{ + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + }, + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + Annotations: map[string]string{ + "node.beta.openyurt.io/autonomy": "true", + }, + }, + }, + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node3", + Annotations: map[string]string{ + "apps.openyurt.io/binding": "true", + }, + }, + }, + } + return nodes +} + +func preparePods() []client.Object { + second1 := int64(300) + second2 := int64(100) + pods := []client.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: metav1.NamespaceDefault, + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "DaemonSet", + }, + }, + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: metav1.NamespaceDefault, + Annotations: map[string]string{ + corev1.MirrorPodAnnotationKey: "03b446125f489d8b04a90de0899657ca", + }, + }, + Spec: corev1.PodSpec{ + Tolerations: []corev1.Toleration{ + { + Key: corev1.TaintNodeNotReady, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + }, + }, + NodeName: "node1", + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod3", + Namespace: metav1.NamespaceDefault, + }, + Spec: corev1.PodSpec{ + Tolerations: []corev1.Toleration{ + { + Key: corev1.TaintNodeNotReady, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + TolerationSeconds: &second1, + }, + { + Key: corev1.TaintNodeUnreachable, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + TolerationSeconds: &second1, + }, + }, + NodeName: "node1", + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod4", + Namespace: metav1.NamespaceDefault, + }, + Spec: corev1.PodSpec{ + Tolerations: []corev1.Toleration{ + { + Key: corev1.TaintNodeNotReady, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + TolerationSeconds: &second2, + }, + }, + }, + }, + } + + return pods +} + +func TestReconcile(t *testing.T) { + pods := preparePods() + nodes := prepareNodes() + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatal("Fail to add kubernetes clint-go custom resource") + } + c := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(pods...).WithObjects(nodes...).Build() + + for i := range TestNodesName { + var req = reconcile.Request{NamespacedName: types.NamespacedName{Name: TestNodesName[i]}} + rsp := ReconcilePodBinding{ + Client: c, + } + + _, err := rsp.Reconcile(context.TODO(), req) + if err != nil { + t.Errorf("Reconcile() error = %v", err) + return + } + + pod := &corev1.Pod{} + err = c.Get(context.TODO(), types.NamespacedName{Namespace: metav1.NamespaceDefault, Name: TestPodsName[i]}, pod) + if err != nil { + continue + } + t.Logf("pod %s Tolerations is %+v", TestPodsName[i], pod.Spec.Tolerations) + } +} + +func TestConfigureTolerationForPod(t *testing.T) { + pods := preparePods() + nodes := prepareNodes() + c := fakeclient.NewClientBuilder().WithObjects(pods...).WithObjects(nodes...).Build() + + second := int64(300) + tests := []struct { + name string + pod *corev1.Pod + tolerationSeconds *int64 + wantErr bool + }{ + { + name: "test1", + pod: pods[0].(*corev1.Pod), + tolerationSeconds: &second, + wantErr: false, + }, + { + name: "test2", + pod: pods[1].(*corev1.Pod), + tolerationSeconds: &second, + wantErr: false, + }, + { + name: "test3", + pod: pods[2].(*corev1.Pod), + tolerationSeconds: &second, + wantErr: false, + }, + { + name: "test4", + pod: pods[3].(*corev1.Pod), + tolerationSeconds: &second, + wantErr: false, + }, + { + name: "test5", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod5", + Namespace: metav1.NamespaceDefault, + }, + }, + tolerationSeconds: &second, + wantErr: true, + }, + { + name: "test6", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod5", + Namespace: metav1.NamespaceDefault, + }, + }, + tolerationSeconds: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &ReconcilePodBinding{ + Client: c, + } + if err := r.configureTolerationForPod(tt.pod, tt.tolerationSeconds); (err != nil) != tt.wantErr { + t.Errorf("configureTolerationForPod() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetPodsAssignedToNode(t *testing.T) { + pods := preparePods() + c := fakeclient.NewClientBuilder().WithObjects(pods...).Build() + tests := []struct { + name string + nodeName string + want []corev1.Pod + wantErr bool + }{ + { + name: "test1", + nodeName: "node1", + want: []corev1.Pod{ + *pods[0].(*corev1.Pod), + *pods[1].(*corev1.Pod), + *pods[2].(*corev1.Pod), + *pods[3].(*corev1.Pod), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &ReconcilePodBinding{ + Client: c, + } + // By the way, the fake client not support ListOptions.FieldSelector, only Namespace and LabelSelector + // For more details, see sigs.k8s.io/controller-runtime@v0.10.3/pkg/client/fake/client.go:366 + got, err := r.getPodsAssignedToNode(tt.nodeName) + if (err != nil) != tt.wantErr { + t.Errorf("getPodsAssignedToNode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getPodsAssignedToNode() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAddOrUpdateTolerationInPodSpec(t *testing.T) { + pods := preparePods() + second := int64(300) + tests := []struct { + name string + pod *corev1.Pod + want bool + }{ + { + name: "toleration1", + pod: pods[0].(*corev1.Pod), + want: true, + }, + { + name: "toleration2", + pod: pods[1].(*corev1.Pod), + want: false, + }, + { + name: "toleration3", + pod: pods[2].(*corev1.Pod), + want: false, + }, + { + name: "toleration4", + pod: pods[3].(*corev1.Pod), + want: true, + }, + } + for _, tt := range tests { + toleration := corev1.Toleration{ + Key: corev1.TaintNodeNotReady, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + TolerationSeconds: &second, + } + if tt.name == "toleration2" { + toleration = corev1.Toleration{ + Key: corev1.TaintNodeNotReady, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + } + } + t.Run(tt.name, func(t *testing.T) { + if got := addOrUpdateTolerationInPodSpec(&tt.pod.Spec, &toleration); got != tt.want { + t.Errorf("addOrUpdateTolerationInPodSpec() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsDaemonSetPodOrStaticPod(t *testing.T) { + pods := preparePods() + tests := []struct { + name string + pod *corev1.Pod + want bool + }{ + { + name: "pod0", + pod: nil, + want: false, + }, + { + name: "pod1", + pod: pods[0].(*corev1.Pod), + want: true, + }, + { + name: "pod2", + pod: pods[1].(*corev1.Pod), + want: true, + }, + { + name: "pod3", + pod: pods[2].(*corev1.Pod), + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isDaemonSetPodOrStaticPod(tt.pod); got != tt.want { + t.Errorf("isDaemonSetPodOrStaticPod() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsPodBoundenToNode(t *testing.T) { + nodes := prepareNodes() + tests := []struct { + name string + node *corev1.Node + want bool + }{ + { + name: "node1", + node: nodes[0].(*corev1.Node), + want: false, + }, + { + name: "node2", + node: nodes[1].(*corev1.Node), + want: true, + }, + { + name: "node3", + node: nodes[2].(*corev1.Node), + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isPodBoundenToNode(tt.node); got != tt.want { + t.Errorf("isPodBoundenToNode() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newReconciler(t *testing.T) { + tests := []struct { + name string + in0 *appconfig.CompletedConfig + mgr manager.Manager + want reconcile.Reconciler + }{ + { + name: "test1", + in0: nil, + mgr: nil, + want: &ReconcilePodBinding{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newReconciler(tt.in0, tt.mgr); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newReconciler() = %v, want %v", got, tt.want) + } + }) + } +} From f67cbde603725140a6fd4f4118942f99a63c62c6 Mon Sep 17 00:00:00 2001 From: rambohe Date: Mon, 26 Jun 2023 16:36:14 +0800 Subject: [PATCH 43/93] add unit test cases for yurthub filters (#1570) --- .../filter/discardcloudservice/filter.go | 6 +- .../filter/discardcloudservice/filter_test.go | 6 +- pkg/yurthub/filter/inclusterconfig/filter.go | 6 +- .../filter/inclusterconfig/filter_test.go | 6 +- pkg/yurthub/filter/initializer/initializer.go | 2 +- .../filter/initializer/initializer_test.go | 137 ++++++++++++++++++ pkg/yurthub/filter/masterservice/filter.go | 6 +- .../filter/masterservice/filter_test.go | 4 +- .../filter/nodeportisolation/filter.go | 6 +- .../filter/nodeportisolation/filter_test.go | 4 +- pkg/yurthub/filter/servicetopology/filter.go | 6 +- .../filter/servicetopology/filter_test.go | 4 +- 12 files changed, 173 insertions(+), 20 deletions(-) create mode 100644 pkg/yurthub/filter/initializer/initializer_test.go diff --git a/pkg/yurthub/filter/discardcloudservice/filter.go b/pkg/yurthub/filter/discardcloudservice/filter.go index 3f6c5a75f56..d8cd75d5759 100644 --- a/pkg/yurthub/filter/discardcloudservice/filter.go +++ b/pkg/yurthub/filter/discardcloudservice/filter.go @@ -36,12 +36,16 @@ var ( // Register registers a filter func Register(filters *filter.Filters) { filters.Register(filter.DiscardCloudServiceFilterName, func() (filter.ObjectFilter, error) { - return &discardCloudServiceFilter{}, nil + return NewDiscardCloudServiceFilter() }) } type discardCloudServiceFilter struct{} +func NewDiscardCloudServiceFilter() (filter.ObjectFilter, error) { + return &discardCloudServiceFilter{}, nil +} + func (sf *discardCloudServiceFilter) Name() string { return filter.DiscardCloudServiceFilterName } diff --git a/pkg/yurthub/filter/discardcloudservice/filter_test.go b/pkg/yurthub/filter/discardcloudservice/filter_test.go index 69c75bb8075..b1d8832fe36 100644 --- a/pkg/yurthub/filter/discardcloudservice/filter_test.go +++ b/pkg/yurthub/filter/discardcloudservice/filter_test.go @@ -30,14 +30,14 @@ import ( ) func TestName(t *testing.T) { - dcsf := &discardCloudServiceFilter{} + dcsf, _ := NewDiscardCloudServiceFilter() if dcsf.Name() != filter.DiscardCloudServiceFilterName { t.Errorf("expect %s, but got %s", filter.DiscardCloudServiceFilterName, dcsf.Name()) } } func TestSupportedResourceAndVerbs(t *testing.T) { - dcsf := &discardCloudServiceFilter{} + dcsf, _ := NewDiscardCloudServiceFilter() rvs := dcsf.SupportedResourceAndVerbs() if len(rvs) != 1 { t.Errorf("supported more than one resources, %v", rvs) @@ -322,7 +322,7 @@ func TestFilter(t *testing.T) { stopCh := make(<-chan struct{}) for k, tt := range testcases { t.Run(k, func(t *testing.T) { - dcsf := &discardCloudServiceFilter{} + dcsf, _ := NewDiscardCloudServiceFilter() newObj := dcsf.Filter(tt.responseObj, stopCh) if tt.expectObj == nil { diff --git a/pkg/yurthub/filter/inclusterconfig/filter.go b/pkg/yurthub/filter/inclusterconfig/filter.go index 8de54c3b4bb..73e5da47fe6 100644 --- a/pkg/yurthub/filter/inclusterconfig/filter.go +++ b/pkg/yurthub/filter/inclusterconfig/filter.go @@ -38,12 +38,16 @@ const ( // Register registers a filter func Register(filters *filter.Filters) { filters.Register(filter.InClusterConfigFilterName, func() (filter.ObjectFilter, error) { - return &inClusterConfigFilter{}, nil + return NewInClusterConfigFilter() }) } type inClusterConfigFilter struct{} +func NewInClusterConfigFilter() (filter.ObjectFilter, error) { + return &inClusterConfigFilter{}, nil +} + func (iccf *inClusterConfigFilter) Name() string { return filter.InClusterConfigFilterName } diff --git a/pkg/yurthub/filter/inclusterconfig/filter_test.go b/pkg/yurthub/filter/inclusterconfig/filter_test.go index 4a015afb1f3..e513b7564c8 100644 --- a/pkg/yurthub/filter/inclusterconfig/filter_test.go +++ b/pkg/yurthub/filter/inclusterconfig/filter_test.go @@ -30,14 +30,14 @@ import ( ) func TestName(t *testing.T) { - iccf := &inClusterConfigFilter{} + iccf, _ := NewInClusterConfigFilter() if iccf.Name() != filter.InClusterConfigFilterName { t.Errorf("expect %s, but got %s", filter.InClusterConfigFilterName, iccf.Name()) } } func TestSupportedResourceAndVerbs(t *testing.T) { - iccf := inClusterConfigFilter{} + iccf, _ := NewInClusterConfigFilter() rvs := iccf.SupportedResourceAndVerbs() if len(rvs) != 1 { t.Errorf("supported more than one resources, %v", rvs) @@ -55,7 +55,7 @@ func TestSupportedResourceAndVerbs(t *testing.T) { } func TestRuntimeObjectFilter(t *testing.T) { - iccf := inClusterConfigFilter{} + iccf, _ := NewInClusterConfigFilter() testcases := map[string]struct { responseObject runtime.Object diff --git a/pkg/yurthub/filter/initializer/initializer.go b/pkg/yurthub/filter/initializer/initializer.go index 796c4a1b0d1..87d0d982185 100644 --- a/pkg/yurthub/filter/initializer/initializer.go +++ b/pkg/yurthub/filter/initializer/initializer.go @@ -70,7 +70,7 @@ type genericFilterInitializer struct { func New(factory informers.SharedInformerFactory, yurtFactory yurtinformers.SharedInformerFactory, kubeClient kubernetes.Interface, - nodeName, nodePoolName, masterServiceHost, masterServicePort string) *genericFilterInitializer { + nodeName, nodePoolName, masterServiceHost, masterServicePort string) filter.Initializer { return &genericFilterInitializer{ factory: factory, yurtFactory: yurtFactory, diff --git a/pkg/yurthub/filter/initializer/initializer_test.go b/pkg/yurthub/filter/initializer/initializer_test.go new file mode 100644 index 00000000000..efda901fb1d --- /dev/null +++ b/pkg/yurthub/filter/initializer/initializer_test.go @@ -0,0 +1,137 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package initializer + +import ( + "errors" + "reflect" + "testing" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes/fake" + + "github.com/openyurtio/openyurt/pkg/yurthub/filter" + "github.com/openyurtio/openyurt/pkg/yurthub/filter/discardcloudservice" + "github.com/openyurtio/openyurt/pkg/yurthub/filter/inclusterconfig" + "github.com/openyurtio/openyurt/pkg/yurthub/filter/masterservice" + "github.com/openyurtio/openyurt/pkg/yurthub/filter/nodeportisolation" + "github.com/openyurtio/openyurt/pkg/yurthub/filter/servicetopology" + yurtfake "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned/fake" + yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" +) + +func TestNew(t *testing.T) { + fakeClient := &fake.Clientset{} + fakeYurtClient := &yurtfake.Clientset{} + sharedFactory := informers.NewSharedInformerFactory(fakeClient, 24*time.Hour) + yurtSharedFactory := yurtinformers.NewSharedInformerFactory(fakeYurtClient, 24*time.Hour) + nodeName := "foo" + nodePoolName := "foo-pool" + masterServiceHost := "127.0.0.1" + masterServicePort := "8080" + + obj := New(sharedFactory, yurtSharedFactory, fakeClient, nodeName, nodePoolName, masterServiceHost, masterServicePort) + _, ok := obj.(filter.Initializer) + if !ok { + t.Errorf("expect a filter Initializer object, but got %v", reflect.TypeOf(obj)) + } +} + +func TestInitialize(t *testing.T) { + testcases := map[string]struct { + fn func() (filter.ObjectFilter, error) + result error + }{ + "init discardcloudservice filter": { + fn: discardcloudservice.NewDiscardCloudServiceFilter, + result: nil, + }, + "init inclusterconfig filter": { + fn: inclusterconfig.NewInClusterConfigFilter, + result: nil, + }, + "init masterservice filter": { + fn: masterservice.NewMasterServiceFilter, + result: nil, + }, + "init nodeportisolation filter": { + fn: nodeportisolation.NewNodePortIsolationFilter, + result: nil, + }, + "init servicetopology filter": { + fn: servicetopology.NewServiceTopologyFilter, + result: nil, + }, + "init errfilter filter": { + fn: NewErrFilter, + result: nodeNameErr, + }, + } + fakeClient := &fake.Clientset{} + fakeYurtClient := &yurtfake.Clientset{} + sharedFactory := informers.NewSharedInformerFactory(fakeClient, 24*time.Hour) + yurtSharedFactory := yurtinformers.NewSharedInformerFactory(fakeYurtClient, 24*time.Hour) + nodeName := "foo" + nodePoolName := "foo-pool" + masterServiceHost := "127.0.0.1" + masterServicePort := "8080" + + obj := New(sharedFactory, yurtSharedFactory, fakeClient, nodeName, nodePoolName, masterServiceHost, masterServicePort) + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + objFilter, _ := tc.fn() + err := obj.Initialize(objFilter) + if tc.result != err { + t.Errorf("expect result error: %v, but got %v", tc.result, err) + } + }) + } +} + +type errFilter struct { + err error +} + +var ( + nodeNameErr = errors.New("node name error") +) + +func NewErrFilter() (filter.ObjectFilter, error) { + return &errFilter{ + err: nodeNameErr, + }, nil +} + +func (ef *errFilter) Name() string { + return "nop" +} + +func (ef *errFilter) SupportedResourceAndVerbs() map[string]sets.String { + return map[string]sets.String{} +} + +func (ef *errFilter) Filter(obj runtime.Object, _ <-chan struct{}) runtime.Object { + return obj +} + +func (ef *errFilter) SetNodeName(nodeName string) error { + return ef.err +} diff --git a/pkg/yurthub/filter/masterservice/filter.go b/pkg/yurthub/filter/masterservice/filter.go index 8351de1a346..336fe791d82 100644 --- a/pkg/yurthub/filter/masterservice/filter.go +++ b/pkg/yurthub/filter/masterservice/filter.go @@ -36,7 +36,7 @@ const ( // Register registers a filter func Register(filters *filter.Filters) { filters.Register(filter.MasterServiceFilterName, func() (filter.ObjectFilter, error) { - return &masterServiceFilter{}, nil + return NewMasterServiceFilter() }) } @@ -45,6 +45,10 @@ type masterServiceFilter struct { port int32 } +func NewMasterServiceFilter() (filter.ObjectFilter, error) { + return &masterServiceFilter{}, nil +} + func (msf *masterServiceFilter) Name() string { return filter.MasterServiceFilterName } diff --git a/pkg/yurthub/filter/masterservice/filter_test.go b/pkg/yurthub/filter/masterservice/filter_test.go index a3e9bbc9ad9..1c24b50a42c 100644 --- a/pkg/yurthub/filter/masterservice/filter_test.go +++ b/pkg/yurthub/filter/masterservice/filter_test.go @@ -30,14 +30,14 @@ import ( ) func TestName(t *testing.T) { - msf := &masterServiceFilter{} + msf, _ := NewMasterServiceFilter() if msf.Name() != filter.MasterServiceFilterName { t.Errorf("expect %s, but got %s", filter.MasterServiceFilterName, msf.Name()) } } func TestSupportedResourceAndVerbs(t *testing.T) { - msf := masterServiceFilter{} + msf, _ := NewMasterServiceFilter() rvs := msf.SupportedResourceAndVerbs() if len(rvs) != 1 { t.Errorf("supported more than one resources, %v", rvs) diff --git a/pkg/yurthub/filter/nodeportisolation/filter.go b/pkg/yurthub/filter/nodeportisolation/filter.go index c4c9ff95407..d21171f0f20 100644 --- a/pkg/yurthub/filter/nodeportisolation/filter.go +++ b/pkg/yurthub/filter/nodeportisolation/filter.go @@ -39,7 +39,7 @@ const ( // Register registers a filter func Register(filters *filter.Filters) { filters.Register(filter.NodePortIsolationName, func() (filter.ObjectFilter, error) { - return &nodePortIsolationFilter{}, nil + return NewNodePortIsolationFilter() }) } @@ -49,6 +49,10 @@ type nodePortIsolationFilter struct { client kubernetes.Interface } +func NewNodePortIsolationFilter() (filter.ObjectFilter, error) { + return &nodePortIsolationFilter{}, nil +} + func (nif *nodePortIsolationFilter) Name() string { return filter.NodePortIsolationName } diff --git a/pkg/yurthub/filter/nodeportisolation/filter_test.go b/pkg/yurthub/filter/nodeportisolation/filter_test.go index 871a0e33e15..99ea95fb637 100644 --- a/pkg/yurthub/filter/nodeportisolation/filter_test.go +++ b/pkg/yurthub/filter/nodeportisolation/filter_test.go @@ -32,14 +32,14 @@ import ( ) func TestName(t *testing.T) { - nif := &nodePortIsolationFilter{} + nif, _ := NewNodePortIsolationFilter() if nif.Name() != filter.NodePortIsolationName { t.Errorf("expect %s, but got %s", filter.NodePortIsolationName, nif.Name()) } } func TestSupportedResourceAndVerbs(t *testing.T) { - nif := &nodePortIsolationFilter{} + nif, _ := NewNodePortIsolationFilter() rvs := nif.SupportedResourceAndVerbs() if len(rvs) != 1 { t.Errorf("supported more than one resources, %v", rvs) diff --git a/pkg/yurthub/filter/servicetopology/filter.go b/pkg/yurthub/filter/servicetopology/filter.go index f791ad7cfdb..209c1f47610 100644 --- a/pkg/yurthub/filter/servicetopology/filter.go +++ b/pkg/yurthub/filter/servicetopology/filter.go @@ -51,12 +51,12 @@ var ( // Register registers a filter func Register(filters *filter.Filters) { filters.Register(filter.ServiceTopologyFilterName, func() (filter.ObjectFilter, error) { - return NewFilter(), nil + return NewServiceTopologyFilter() }) } -func NewFilter() *serviceTopologyFilter { - return &serviceTopologyFilter{} +func NewServiceTopologyFilter() (filter.ObjectFilter, error) { + return &serviceTopologyFilter{}, nil } type serviceTopologyFilter struct { diff --git a/pkg/yurthub/filter/servicetopology/filter_test.go b/pkg/yurthub/filter/servicetopology/filter_test.go index b86e7c5d074..3e234d92abf 100644 --- a/pkg/yurthub/filter/servicetopology/filter_test.go +++ b/pkg/yurthub/filter/servicetopology/filter_test.go @@ -38,14 +38,14 @@ import ( ) func TestName(t *testing.T) { - stf := &serviceTopologyFilter{} + stf, _ := NewServiceTopologyFilter() if stf.Name() != filter.ServiceTopologyFilterName { t.Errorf("expect %s, but got %s", filter.ServiceTopologyFilterName, stf.Name()) } } func TestSupportedResourceAndVerbs(t *testing.T) { - stf := &serviceTopologyFilter{} + stf, _ := NewServiceTopologyFilter() rvs := stf.SupportedResourceAndVerbs() if len(rvs) != 2 { t.Errorf("supported not two resources, %v", rvs) From f9d80259680cbeb2e46e173bd3f45c057e69c7cc Mon Sep 17 00:00:00 2001 From: rambohe Date: Mon, 26 Jun 2023 16:41:14 +0800 Subject: [PATCH 44/93] add unit testcases for controller utils (#1569) --- pkg/controller/util/controller_utils_test.go | 57 ++++++++++++++++++++ pkg/controller/util/statefulset_utils.go | 46 ---------------- pkg/controller/util/tools.go | 14 ----- 3 files changed, 57 insertions(+), 60 deletions(-) create mode 100644 pkg/controller/util/controller_utils_test.go delete mode 100644 pkg/controller/util/statefulset_utils.go diff --git a/pkg/controller/util/controller_utils_test.go b/pkg/controller/util/controller_utils_test.go new file mode 100644 index 00000000000..6eb75008e35 --- /dev/null +++ b/pkg/controller/util/controller_utils_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import "testing" + +func TestIsControllerEnabled(t *testing.T) { + testcases := map[string]struct { + name string + controllers []string + result bool + }{ + "enable specified controller": { + name: "foo", + controllers: []string{"foo", "bar"}, + result: true, + }, + "disable specified controller": { + name: "foo", + controllers: []string{"-foo", "bar"}, + result: false, + }, + "enable controller in default": { + name: "foo", + controllers: []string{"bar", "*"}, + result: true, + }, + "controller doesn't exist": { + name: "unknown", + controllers: []string{"foo", "bar"}, + result: false, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + result := IsControllerEnabled(tc.name, tc.controllers) + if tc.result != result { + t.Errorf("expect controller enabled: %v, but got %v", tc.result, result) + } + }) + } +} diff --git a/pkg/controller/util/statefulset_utils.go b/pkg/controller/util/statefulset_utils.go deleted file mode 100644 index 21a5c112eab..00000000000 --- a/pkg/controller/util/statefulset_utils.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright 2020 The OpenYurt Authors. -Copyright 2019 The Kruise Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "regexp" - "strconv" - - corev1 "k8s.io/api/core/v1" -) - -var statefulPodRegex = regexp.MustCompile("(.*)-([0-9]+)$") - -func GetOrdinal(pod *corev1.Pod) int32 { - _, ordinal := getParentNameAndOrdinal(pod) - return ordinal -} - -func getParentNameAndOrdinal(pod *corev1.Pod) (string, int32) { - parent := "" - var ordinal int32 = -1 - subMatches := statefulPodRegex.FindStringSubmatch(pod.Name) - if len(subMatches) < 3 { - return parent, ordinal - } - parent = subMatches[1] - if i, err := strconv.ParseInt(subMatches[2], 10, 32); err == nil { - ordinal = int32(i) - } - return parent, ordinal -} diff --git a/pkg/controller/util/tools.go b/pkg/controller/util/tools.go index ab2e89ca892..0a199cee4e4 100644 --- a/pkg/controller/util/tools.go +++ b/pkg/controller/util/tools.go @@ -62,17 +62,3 @@ func SlowStartBatch(count int, initialBatchSize int, fn func(index int) error) ( } return successes, nil } - -// CheckDuplicate finds if there are duplicated items in a list. -func CheckDuplicate(list []string) []string { - tmpMap := make(map[string]struct{}) - var dupList []string - for _, name := range list { - if _, ok := tmpMap[name]; ok { - dupList = append(dupList, name) - } else { - tmpMap[name] = struct{}{} - } - } - return dupList -} From 211cb4f08b33baaf3da83d04f3f0f4a2447dcb3c Mon Sep 17 00:00:00 2001 From: Tomoya Fujita Date: Sat, 1 Jul 2023 20:31:15 -0700 Subject: [PATCH 45/93] change access permission to default in general. (#1576) Signed-off-by: Tomoya Fujita --- pkg/yurtadm/util/kubernetes/kubernetes.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/yurtadm/util/kubernetes/kubernetes.go b/pkg/yurtadm/util/kubernetes/kubernetes.go index 6e0710b44ca..17a0c51250b 100644 --- a/pkg/yurtadm/util/kubernetes/kubernetes.go +++ b/pkg/yurtadm/util/kubernetes/kubernetes.go @@ -179,7 +179,7 @@ func CheckAndInstallKubernetesCni(reuseCNIBin bool) error { klog.V(1).Infof("Skip download cni, use already exist file: %s", savePath) } - if err := os.MkdirAll(constants.KubeCniDir, 0600); err != nil { + if err := os.MkdirAll(constants.KubeCniDir, 0755); err != nil { return err } if err := util.Untar(savePath, constants.KubeCniDir); err != nil { @@ -301,7 +301,7 @@ func SetKubeletUnitConfig() error { } } - if err := os.WriteFile(constants.KubeletServiceConfPath, []byte(constants.KubeletUnitConfig), 0600); err != nil { + if err := os.WriteFile(constants.KubeletServiceConfPath, []byte(constants.KubeletUnitConfig), 0640); err != nil { return err } From 41647c4c04c1c4100b70a594a2f80f621968b6c4 Mon Sep 17 00:00:00 2001 From: rambohe Date: Tue, 4 Jul 2023 17:03:34 +0800 Subject: [PATCH 46/93] feat: add yurtadm binaries release workflow (#1577) --- .../workflows/{release.yaml => registry.yaml} | 2 +- .github/workflows/release-assets.yaml | 43 +++++++++++++++++ .goreleaser.yaml | 46 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) rename .github/workflows/{release.yaml => registry.yaml} (99%) create mode 100644 .github/workflows/release-assets.yaml create mode 100644 .goreleaser.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/registry.yaml similarity index 99% rename from .github/workflows/release.yaml rename to .github/workflows/registry.yaml index 1bb03ffafec..f39b3691db9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/registry.yaml @@ -1,4 +1,4 @@ -name: Release +name: Release Images on: push: diff --git a/.github/workflows/release-assets.yaml b/.github/workflows/release-assets.yaml new file mode 100644 index 00000000000..074e08931d6 --- /dev/null +++ b/.github/workflows/release-assets.yaml @@ -0,0 +1,43 @@ +name: Release Assets + +on: + push: + tags: + - "v*" + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + goreleaser: + if: github.repository == 'openyurtio/openyurt' + permissions: + contents: write + actions: read + checks: write + issues: read + packages: write + pull-requests: read + repository-projects: read + statuses: read + runs-on: ubuntu-22.04 + name: goreleaser + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.18 + cache: true + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000000..bd0286b733a --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,46 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +builds: + - id: yurtadm + binary: yurtadm + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + main: ./cmd/yurtadm/yurtadm.go + ldflags: + - -s -w -X github.com/openyurtio/openyurt/pkg/projectinfo.gitVersion={{ .Tag }} -X github.com/openyurtio/openyurt/pkg/projectinfo.gitCommit={{ .ShortCommit }} -X github.com/openyurtio/openyurt/pkg/projectinfo.buildDate={{ .Date }} + env: + - CGO_ENABLED=0 + +archives: + - format: tar.gz + id: yurtadm-tgz + wrap_in_directory: '{{ .Os }}-{{ .Arch }}' + builds: + - yurtadm + name_template: '{{ .ArtifactName }}-{{ .Tag }}-{{ .Os }}-{{ .Arch }}' + files: [ LICENSE, README.md ] + - format: zip + id: yurtadm-zip + builds: + - yurtadm + wrap_in_directory: '{{ .Os }}-{{ .Arch }}' + name_template: '{{ .ArtifactName }}-{{ .Tag }}-{{ .Os }}-{{ .Arch }}' + files: [ LICENSE, README.md ] + +checksum: + name_template: 'sha256sums.txt' +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +# The lines beneath this are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj From 6a222380155e77508d03cf187517b7891f1b5a02 Mon Sep 17 00:00:00 2001 From: Armin Schlegel Date: Wed, 5 Jul 2023 04:43:12 +0200 Subject: [PATCH 47/93] build: added github registry (#1578) Signed-off-by: Armin Schlegel wip --- .github/workflows/registry.yaml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/registry.yaml b/.github/workflows/registry.yaml index f39b3691db9..5d03cab7d6f 100644 --- a/.github/workflows/registry.yaml +++ b/.github/workflows/registry.yaml @@ -6,11 +6,12 @@ on: - "v*" schedule: # run at UTC 1:30 every day - - cron: '30 1 * * *' + - cron: "30 1 * * *" workflow_dispatch: {} env: ALI_REGISTRY: registry.cn-hangzhou.aliyuncs.com/openyurt + GITHUB_REGISTRY: ghcr.io/openyurtio/openyurt jobs: docker-push: @@ -65,4 +66,31 @@ jobs: username: ${{ secrets.ALI_REGISTRY_USERNAME }} password: ${{ secrets.ALI_REGISTRY_PASSWORD }} - name: Release - run: make docker-push TARGET_PLATFORMS=linux/amd64,linux/arm64,linux/arm/v7 IMAGE_REPO=${{ env.ALI_REGISTRY }} IMAGE_TAG=${{ steps.get_version.outputs.VERSION }} \ No newline at end of file + run: make docker-push TARGET_PLATFORMS=linux/amd64,linux/arm64,linux/arm/v7 IMAGE_REPO=${{ env.ALI_REGISTRY }} IMAGE_TAG=${{ steps.get_version.outputs.VERSION }} + docker-push-github-registry: + if: github.repository == 'openyurtio/openyurt' + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Get the version + id: get_version + run: | + VERSION=${GITHUB_REF#refs/tags/} + if [[ ${GITHUB_REF} == "refs/heads/master" ]]; then + VERSION=latest + fi + echo ::set-output name=VERSION::${VERSION} + - name: Install Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ secrets.GH_REGISTRY_USERNAME }} + password: ${{ secrets.GH_REGISTRY_PASSWORD }} + - name: Release + run: make docker-push TARGET_PLATFORMS=linux/amd64,linux/arm64,linux/arm/v7 IMAGE_REPO=${{ env.GITHUB_REGISTRY }} IMAGE_TAG=${{ steps.get_version.outputs.VERSION }} From 9a97791e9960653042b207202b93c202ea9882b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=91=B8=E9=B1=BC=E5=96=B5?= <1254297317@qq.com> Date: Thu, 6 Jul 2023 09:51:15 +0800 Subject: [PATCH 48/93] feat: support edgex minnesota through auto-collector (#1582) * feat: support edgex minnesota through auto-collector Signed-off-by: LavenderQAQ <1254297317@qq.com> * fix: remove the mount of the ekuiper etc directory in minnesota Signed-off-by: LavenderQAQ <1254297317@qq.com> --------- Signed-off-by: LavenderQAQ <1254297317@qq.com> --- .../config/EdgeXConfig/config-nosecty.json | 3616 +++-- .../config/EdgeXConfig/config.json | 11816 +++++++++++++++- .../config/EdgeXConfig/manifest.yaml | 11 +- 3 files changed, 14111 insertions(+), 1332 deletions(-) diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json b/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json index eff97d58ac7..30b3908cec3 100644 --- a/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json +++ b/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json @@ -1,11 +1,11 @@ { "versions": [ { - "versionName": "levski", + "versionName": "kamakura", "configMaps": [ { "metadata": { - "name": "common-variable-levski", + "name": "common-variable-kamakura", "creationTimestamp": null }, "data": { @@ -23,632 +23,745 @@ ], "components": [ { - "name": "edgex-support-scheduler", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59701", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-app-rules-engine" } }, "spec": { "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/support-scheduler:2.3.0", + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.2.0", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], "env": [ { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" }, { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "value": "edgex-app-rules-engine" }, { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-5563", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-data" } }, "spec": { "containers": [ { - "name": "edgex-app-rules-engine", - "image": "openyurt/app-service-configurable:2.3.0", + "name": "edgex-core-data", + "image": "openyurt/core-data:2.2.0", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], "env": [ - { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" - }, - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" - }, { "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" + "value": "edgex-core-data" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-59986", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-device-rest" } }, "spec": { "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/core-metadata:2.3.0", + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.2.0", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "value": "edgex-device-rest" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-58890", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 58890, + "targetPort": 58890 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-sys-mgmt-agent" } }, "spec": { - "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/consul:1.13.2", + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.2.0", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-58890", + "containerPort": 58890, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59882", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:2.3.0", + "name": "edgex-core-command", + "image": "openyurt/core-command:2.2.0", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-data" + "value": "edgex-core-command" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "edgex-core-command", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-59861", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-support-scheduler" } }, "spec": { "containers": [ { - "name": "edgex-core-command", - "image": "openyurt/core-command:2.3.0", + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.2.0", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-command" + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" }, { - "name": "MESSAGEQUEUE_EXTERNAL_URL", - "value": "tcp://edgex-mqtt-broker:1883" + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" }, { - "name": "MESSAGEQUEUE_INTERNAL_HOST", - "value": "edgex-redis" + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-ui-go", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-4000", + "name": "tcp-59881", "protocol": "TCP", - "port": 4000, - "targetPort": 4000 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-ui-go" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-ui-go" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-ui-go" + "app": "edgex-core-metadata" } }, "spec": { "containers": [ { - "name": "edgex-ui-go", - "image": "openyurt/edgex-ui:2.3.0", + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.2.0", "ports": [ { - "name": "tcp-4000", - "containerPort": 4000, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-ui-go" + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-ui-go" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-59720", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-kuiper" } }, "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/sys-mgmt-agent:2.3.0", + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.4.4-alpine", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" }, { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" }, { - "name": "METRICSMECHANISM", - "value": "executor" + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" } ], "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-6379", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-redis" } }, "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/device-rest:2.3.0", + "name": "edgex-redis", + "image": "openyurt/redis:6.2.6-alpine", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], - "env": [ + "resources": {}, + "volumeMounts": [ { - "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "name": "db-data", + "mountPath": "/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "openyurt/consul:1.10.10", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } } ], "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-core-consul" } }, "strategy": {} @@ -686,7 +799,7 @@ "containers": [ { "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:2.3.0", + "image": "openyurt/device-virtual:2.2.0", "ports": [ { "name": "tcp-59900", @@ -697,7 +810,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], @@ -749,7 +862,7 @@ "containers": [ { "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:2.3.0", + "image": "openyurt/support-notifications:2.2.0", "ports": [ { "name": "tcp-59860", @@ -760,7 +873,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], @@ -781,184 +894,57 @@ } }, { - "name": "edgex-redis", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-4000", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 4000, + "targetPort": 4000 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-ui-go" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-ui-go" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-ui-go" } }, "spec": { - "volumes": [ - { - "name": "db-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:7.0.5-alpine", - "ports": [ - { - "name": "tcp-6379", - "containerPort": 6379, - "protocol": "TCP" - } - ], - "envFrom": [ - { - "configMapRef": { - "name": "common-variable-levski" - } - } - ], - "resources": {}, - "volumeMounts": [ - { - "name": "db-data", - "mountPath": "/data" - } - ], - "imagePullPolicy": "IfNotPresent" - } - ], - "hostname": "edgex-redis" - } - }, - "strategy": {} - } - }, - { - "name": "edgex-kuiper", - "service": { - "ports": [ - { - "name": "tcp-59720", - "protocol": "TCP", - "port": 59720, - "targetPort": 59720 - } - ], - "selector": { - "app": "edgex-kuiper" - } - }, - "deployment": { - "selector": { - "matchLabels": { - "app": "edgex-kuiper" - } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-kuiper" - } - }, - "spec": { - "volumes": [ - { - "name": "kuiper-data", - "emptyDir": {} - } - ], - "containers": [ - { - "name": "edgex-kuiper", - "image": "openyurt/ekuiper:1.7.1-alpine", + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:2.2.0", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, + "name": "tcp-4000", + "containerPort": 4000, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variable-kamakura" } } ], - "env": [ - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", - "value": "redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, - { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" - } - ], "resources": {}, - "volumeMounts": [ - { - "name": "kuiper-data", - "mountPath": "/kuiper/data" - } - ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-ui-go" } }, "strategy": {} @@ -989,42 +975,42 @@ ], "components": [ { - "name": "edgex-support-notifications", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59900", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-device-virtual" } }, "spec": { "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:2.1.1", + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.1.1", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], @@ -1038,56 +1024,56 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-device-virtual" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-4000", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 4000, + "targetPort": 4000 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-ui-go" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-ui-go" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-ui-go" } }, "spec": { "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/sys-mgmt-agent:2.1.1", + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:2.1.0", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-4000", + "containerPort": 4000, "protocol": "TCP" } ], @@ -1098,67 +1084,53 @@ } } ], - "env": [ - { - "name": "METRICSMECHANISM", - "value": "executor" - }, - { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" - } - ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-ui-go" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-59882", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/device-rest:2.1.1", + "name": "edgex-core-command", + "image": "openyurt/core-command:2.1.1", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], @@ -1172,56 +1144,56 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-core-command" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-59861", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-support-scheduler" } }, "spec": { "containers": [ { - "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:2.1.1", + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.1.1", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], @@ -1235,56 +1207,74 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-8500", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-core-consul" } }, "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/core-metadata:2.1.1", + "name": "edgex-core-consul", + "image": "openyurt/consul:1.10.3", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], @@ -1295,21 +1285,21 @@ } } ], - "env": [ + "resources": {}, + "volumeMounts": [ { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "name": "consul-config", + "mountPath": "/consul/config" }, { - "name": "SERVICE_HOST", - "value": "edgex-core-metadata" + "name": "consul-data", + "mountPath": "/consul/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-core-consul" } }, "strategy": {} @@ -1347,7 +1337,7 @@ "containers": [ { "name": "edgex-app-rules-engine", - "image": "openyurt/app-service-configurable:2.1.1", + "image": "openyurt/app-service-configurable:2.1.2", "ports": [ { "name": "tcp-59701", @@ -1364,20 +1354,20 @@ ], "env": [ { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" + "name": "EDGEX_PROFILE", + "value": "rules-engine" }, { "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", "value": "edgex-redis" }, - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, { "name": "SERVICE_HOST", "value": "edgex-app-rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" } ], "resources": {}, @@ -1390,6 +1380,69 @@ "strategy": {} } }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.1.1", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, { "name": "edgex-redis", "service": { @@ -1460,42 +1513,42 @@ } }, { - "name": "edgex-core-command", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-58890", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 58890, + "targetPort": 58890 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "containers": [ { - "name": "edgex-core-command", - "image": "openyurt/core-command:2.1.1", + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.1.1", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-58890", + "containerPort": 58890, "protocol": "TCP" } ], @@ -1507,69 +1560,66 @@ } ], "env": [ + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + }, { "name": "SERVICE_HOST", - "value": "edgex-core-command" + "value": "edgex-sys-mgmt-agent" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59860", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:2.1.1", + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.1.1", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], @@ -1583,56 +1633,56 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-data" + "value": "edgex-support-notifications" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59881", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-core-metadata" } }, "spec": { "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/support-scheduler:2.1.1", + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.1.1", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], @@ -1645,66 +1695,136 @@ ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" }, { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-kuiper", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-5563", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "spec": { - "volumes": [ - { - "name": "kuiper-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-kuiper", + "name": "edgex-core-data", + "image": "openyurt/core-data:2.1.1", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", "image": "openyurt/ekuiper:1.4.4-alpine", "ports": [ { @@ -1722,48 +1842,48 @@ ], "env": [ { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "name": "EDGEX__DEFAULT__TYPE", "value": "redis" }, { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" }, { - "name": "EDGEX__DEFAULT__PORT", + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", "value": "6379" }, { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", "value": "redis" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", - "value": "edgex-redis" + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" }, { - "name": "EDGEX__DEFAULT__TYPE", + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", "value": "redis" }, { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" }, { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" } ], "resources": {}, @@ -1781,6 +1901,93 @@ }, "strategy": {} } + } + ] + }, + { + "versionName": "levski", + "configMaps": [ + { + "metadata": { + "name": "common-variable-levski", + "creationTimestamp": null + }, + "data": { + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "false", + "MESSAGEQUEUE_HOST": "edgex-redis", + "REGISTRY_HOST": "edgex-core-consul" + } + } + ], + "components": [ + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.3.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } }, { "name": "edgex-ui-go", @@ -1814,7 +2021,7 @@ "containers": [ { "name": "edgex-ui-go", - "image": "openyurt/edgex-ui:2.1.0", + "image": "openyurt/edgex-ui:2.3.0", "ports": [ { "name": "tcp-4000", @@ -1825,10 +2032,16 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variable-levski" } } ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-ui-go" + } + ], "resources": {}, "imagePullPolicy": "IfNotPresent" } @@ -1881,7 +2094,7 @@ "containers": [ { "name": "edgex-core-consul", - "image": "openyurt/consul:1.10.3", + "image": "openyurt/consul:1.13.2", "ports": [ { "name": "tcp-8500", @@ -1892,7 +2105,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variable-levski" } } ], @@ -1915,64 +2128,571 @@ }, "strategy": {} } - } - ] - }, - { - "versionName": "kamakura", - "configMaps": [ - { - "metadata": { - "name": "common-variable-kamakura", - "creationTimestamp": null - }, - "data": { - "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", - "CLIENTS_CORE_DATA_HOST": "edgex-core-data", - "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", - "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", - "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", - "DATABASES_PRIMARY_HOST": "edgex-redis", - "EDGEX_SECURITY_SECRET_STORE": "false", - "MESSAGEQUEUE_HOST": "edgex-redis", - "REGISTRY_HOST": "edgex-core-consul" - } - } - ], - "components": [ + }, { - "name": "edgex-core-command", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-59861", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-support-scheduler" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.3.0", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.3.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.3.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.3.1", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.3.0", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.7.1-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "openyurt/redis:7.0.5-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "db-data", + "mountPath": "/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" } }, "spec": { "containers": [ { "name": "edgex-core-command", - "image": "openyurt/core-command:2.2.0", + "image": "openyurt/core-command:2.3.0", "ports": [ { "name": "tcp-59882", @@ -1983,14 +2703,22 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-levski" } } ], "env": [ + { + "name": "MESSAGEQUEUE_EXTERNAL_URL", + "value": "tcp://edgex-mqtt-broker:1883" + }, { "name": "SERVICE_HOST", "value": "edgex-core-command" + }, + { + "name": "MESSAGEQUEUE_INTERNAL_HOST", + "value": "edgex-redis" } ], "resources": {}, @@ -2004,725 +2732,967 @@ } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.3.0", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-data", + "image": "openyurt/core-data:2.3.0", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "minnesota", + "configMaps": [ + { + "metadata": { + "name": "common-variable-minnesota", + "creationTimestamp": null + } + } + ], + "components": [ + { + "name": "edgex-core-common-config-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-common-config-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-common-config-bootstrapper" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-core-common-config-bootstrapper", + "image": "openyurt/core-common-config-bootstrapper:3.0.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "APP_SERVICES_CLIENTS_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "DEVICE_SERVICES_CLIENTS_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" + }, + { + "name": "ALL_SERVICES_DATABASE_HOST", + "value": "edgex-redis" + }, + { + "name": "ALL_SERVICES_MESSAGEBUS_HOST", + "value": "edgex-redis" + }, + { + "name": "ALL_SERVICES_REGISTRY_HOST", + "value": "edgex-core-consul" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-common-config-bootstrapper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-virtual" } }, "spec": { "containers": [ { - "name": "edgex-app-rules-engine", - "image": "openyurt/app-service-configurable:2.2.0", + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:3.0.0", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], "env": [ { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" - }, - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" }, { - "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-59861", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-support-scheduler" } }, "spec": { "containers": [ { - "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:2.2.0", + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:3.0.0", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], "env": [ + { + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, { "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "value": "edgex-support-scheduler" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59882", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:2.2.0", + "name": "edgex-core-command", + "image": "openyurt/core-command:3.0.0", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], "env": [ + { + "name": "EXTERNALMQTT_URL", + "value": "tcp://edgex-mqtt-broker:1883" + }, { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-core-command" + }, + { + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-59880", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-core-data" } }, "spec": { "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/core-metadata:2.2.0", + "name": "edgex-core-data", + "image": "openyurt/core-data:3.0.0", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" + "value": "edgex-core-data" }, { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-kuiper", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-59881", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-core-metadata" } }, "spec": { - "volumes": [ - { - "name": "kuiper-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-kuiper", - "image": "openyurt/ekuiper:1.4.4-alpine", + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:3.0.0", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], "env": [ { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" - }, - { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", - "value": "edgex-redis" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" }, { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" } ], "resources": {}, - "volumeMounts": [ - { - "name": "kuiper-data", - "mountPath": "/kuiper/data" - } - ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-6379", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-redis" } }, "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/sys-mgmt-agent:2.2.0", + "name": "edgex-redis", + "image": "openyurt/redis:7.0.11-alpine", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], - "envFrom": [ - { - "configMapRef": { - "name": "common-variable-kamakura" - } - } - ], - "env": [ - { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" - }, - { - "name": "METRICSMECHANISM", - "value": "executor" - }, + "envFrom": [ { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" + "configMapRef": { + "name": "common-variable-minnesota" + } } ], "resources": {}, + "volumeMounts": [ + { + "name": "db-data", + "mountPath": "/data" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-ui-go", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-4000", + "name": "tcp-59701", "protocol": "TCP", - "port": 4000, - "targetPort": 4000 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-ui-go" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-ui-go" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-ui-go" + "app": "edgex-app-rules-engine" } }, "spec": { "containers": [ { - "name": "edgex-ui-go", - "image": "openyurt/edgex-ui:2.2.0", + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:3.0.0", "ports": [ { - "name": "tcp-4000", - "containerPort": 4000, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], + "env": [ + { + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + } + ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-ui-go" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59720", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-kuiper" } }, "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + }, + { + "name": "kuiper-log", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:2.2.0", + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.9.2-alpine", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-data" + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "edgex/rules-events" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" } ], "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + }, + { + "name": "kuiper-log", + "mountPath": "/kuiper/log" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-4000", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 4000, + "targetPort": 4000 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "spec": { "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/support-scheduler:2.2.0", + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:3.0.0", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-4000", + "containerPort": 4000, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], "env": [ { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" }, { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "value": "edgex-ui-go" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-ui-go" } }, "strategy": {} } }, { - "name": "edgex-redis", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-8500", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-core-consul" } }, "spec": { "volumes": [ { - "name": "db-data", + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", "emptyDir": {} } ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:6.2.6-alpine", + "name": "edgex-core-consul", + "image": "openyurt/consul:1.15.2", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], "resources": {}, "volumeMounts": [ { - "name": "db-data", - "mountPath": "/data" + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-core-consul" } }, "strategy": {} @@ -2760,7 +3730,7 @@ "containers": [ { "name": "edgex-device-rest", - "image": "openyurt/device-rest:2.2.0", + "image": "openyurt/device-rest:3.0.0", "ports": [ { "name": "tcp-59986", @@ -2771,7 +3741,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], @@ -2779,6 +3749,10 @@ { "name": "SERVICE_HOST", "value": "edgex-device-rest" + }, + { + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" } ], "resources": {}, @@ -2792,77 +3766,67 @@ } }, { - "name": "edgex-core-consul", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-59860", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-support-notifications" } }, "spec": { - "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/consul:1.10.10", + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:3.0.0", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variable-minnesota" } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "EDGEX_SECURITY_SECRET_STORE", + "value": "false" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-support-notifications" } }, "strategy": {} @@ -2893,42 +3857,48 @@ ], "components": [ { - "name": "edgex-core-command", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-59720", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-kuiper" } }, "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-core-command", - "image": "openyurt/core-command:2.0.0", + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.3.0-alpine", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], @@ -2941,57 +3911,97 @@ ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-command" + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" } ], "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-8500", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/sys-mgmt-agent:2.0.0", + "name": "edgex-core-consul", + "image": "openyurt/consul:1.9.5", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], @@ -3002,67 +4012,63 @@ } } ], - "env": [ - { - "name": "METRICSMECHANISM", - "value": "executor" - }, + "resources": {}, + "volumeMounts": [ { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "consul-config", + "mountPath": "/consul/config" }, { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" + "name": "consul-data", + "mountPath": "/consul/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59986", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-device-rest" } }, "spec": { "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:2.0.0", + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.0.0", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], @@ -3076,14 +4082,14 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-device-rest" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-device-rest" } }, "strategy": {} @@ -3142,11 +4148,11 @@ "value": "edgex-support-scheduler" }, { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", "value": "edgex-core-data" }, { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "name": "INTERVALACTIONS_SCRUBAGED_HOST", "value": "edgex-core-data" } ], @@ -3161,52 +4167,48 @@ } }, { - "name": "edgex-core-consul", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-6379", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "spec": { "volumes": [ { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", + "name": "db-data", "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/consul:1.9.5", + "name": "edgex-redis", + "image": "openyurt/redis:6.2.4-alpine", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], @@ -3220,165 +4222,130 @@ "resources": {}, "volumeMounts": [ { - "name": "consul-config", - "mountPath": "/consul/config" - }, - { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "db-data", + "mountPath": "/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-kuiper", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-5563", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "spec": { - "volumes": [ - { - "name": "kuiper-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-kuiper", - "image": "openyurt/ekuiper:1.3.0-alpine", + "name": "edgex-core-data", + "image": "openyurt/core-data:2.0.0", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, - "protocol": "TCP" - } - ], - "envFrom": [ - { - "configMapRef": { - "name": "common-variable-ireland" - } - } - ], - "env": [ - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" }, { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" } ], - "resources": {}, - "volumeMounts": [ + "envFrom": [ { - "name": "kuiper-data", - "mountPath": "/kuiper/data" + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-redis", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-59900", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-device-virtual" } }, "spec": { - "volumes": [ - { - "name": "db-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:6.2.4-alpine", + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.0.0", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], @@ -3389,59 +4356,59 @@ } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "db-data", - "mountPath": "/data" + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-59881", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-metadata" } }, "spec": { "containers": [ { - "name": "edgex-app-rules-engine", - "image": "openyurt/app-service-configurable:2.0.1", + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.0.0", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], @@ -3454,69 +4421,61 @@ ], "env": [ { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" - }, - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" }, { - "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-59701", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-app-rules-engine" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/device-rest:2.0.0", + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.0.1", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], @@ -3530,56 +4489,68 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-app-rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-58890", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 58890, + "targetPort": 58890 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "containers": [ { - "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:2.0.0", + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.0.0", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-58890", + "containerPort": 58890, "protocol": "TCP" } ], @@ -3591,58 +4562,66 @@ } ], "env": [ + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, { "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "value": "edgex-sys-mgmt-agent" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-59882", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/core-metadata:2.0.0", + "name": "edgex-core-command", + "image": "openyurt/core-command:2.0.0", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], @@ -3656,71 +4635,56 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "value": "edgex-core-command" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59860", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:2.0.0", + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.0.0", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], @@ -3734,14 +4698,14 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-data" + "value": "edgex-support-notifications" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-support-notifications" } }, "strategy": {} @@ -3776,48 +4740,42 @@ ], "components": [ { - "name": "edgex-redis", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-48090", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 48090, + "targetPort": 48090 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-sys-mgmt-agent" } }, "spec": { - "volumes": [ - { - "name": "db-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:6.0.9-alpine", + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/docker-sys-mgmt-agent-go:1.3.1", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-48090", + "containerPort": 48090, "protocol": "TCP" } ], @@ -3828,59 +4786,78 @@ } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "db-data", - "mountPath": "/data" + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-48085", + "name": "tcp-5563", "protocol": "TCP", - "port": 48085, - "targetPort": 48085 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-48080", + "protocol": "TCP", + "port": 48080, + "targetPort": 48080 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-core-data" } }, "spec": { "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/docker-support-scheduler-go:1.3.1", + "name": "edgex-core-data", + "image": "openyurt/docker-core-data-go:1.3.1", "ports": [ { - "name": "tcp-48085", - "containerPort": 48085, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-48080", + "containerPort": 48080, "protocol": "TCP" } ], @@ -3894,14 +4871,6 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" - }, - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", "value": "edgex-core-data" } ], @@ -3909,63 +4878,49 @@ "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-49986", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 49986, + "targetPort": 49986 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-device-rest" } }, "spec": { - "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - }, - { - "name": "consul-scripts", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/docker-edgex-consul:1.3.0", + "name": "edgex-device-rest", + "image": "openyurt/docker-device-rest-go:1.2.1", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-49986", + "containerPort": 49986, "protocol": "TCP" } ], @@ -3978,86 +4933,57 @@ ], "env": [ { - "name": "EDGEX_DB", - "value": "redis" - }, - { - "name": "EDGEX_SECURE", - "value": "false" + "name": "SERVICE_HOST", + "value": "edgex-device-rest" } ], "resources": {}, - "volumeMounts": [ - { - "name": "consul-config", - "mountPath": "/consul/config" - }, - { - "name": "consul-data", - "mountPath": "/consul/data" - }, - { - "name": "consul-scripts", - "mountPath": "/consul/scripts" - } - ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-kuiper", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-20498", - "protocol": "TCP", - "port": 20498, - "targetPort": 20498 - }, - { - "name": "tcp-48075", + "name": "tcp-48081", "protocol": "TCP", - "port": 48075, - "targetPort": 48075 + "port": 48081, + "targetPort": 48081 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-core-metadata" } }, "spec": { "containers": [ { - "name": "edgex-kuiper", - "image": "openyurt/kuiper:1.1.1-alpine", + "name": "edgex-core-metadata", + "image": "openyurt/docker-core-metadata-go:1.3.1", "ports": [ { - "name": "tcp-20498", - "containerPort": 20498, - "protocol": "TCP" - }, - { - "name": "tcp-48075", - "containerPort": 48075, + "name": "tcp-48081", + "containerPort": 48081, "protocol": "TCP" } ], @@ -4070,39 +4996,19 @@ ], "env": [ { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "48075" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "5566" - }, - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "tcp" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-app-service-configurable-rules" - }, - { - "name": "EDGEX__DEFAULT__SERVICESERVER", - "value": "http://edgex-core-data:48080" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" }, { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "events" + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-core-metadata" } }, "strategy": {} @@ -4235,105 +5141,140 @@ } }, { - "name": "edgex-support-notifications", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-48060", + "name": "tcp-20498", "protocol": "TCP", - "port": 48060, - "targetPort": 48060 + "port": 20498, + "targetPort": 20498 + }, + { + "name": "tcp-48075", + "protocol": "TCP", + "port": 48075, + "targetPort": 48075 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-kuiper" } }, "spec": { "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/docker-support-notifications-go:1.3.1", + "name": "edgex-kuiper", + "image": "openyurt/kuiper:1.1.1-alpine", "ports": [ { - "name": "tcp-48060", - "containerPort": 48060, - "protocol": "TCP" - } - ], - "envFrom": [ + "name": "tcp-20498", + "containerPort": 20498, + "protocol": "TCP" + }, + { + "name": "tcp-48075", + "containerPort": 48075, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-app-service-configurable-rules" + }, + { + "name": "EDGEX__DEFAULT__SERVICESERVER", + "value": "http://edgex-core-data:48080" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "events" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "48075" + }, { - "configMapRef": { - "name": "common-variable-hanoi" - } - } - ], - "env": [ + "name": "EDGEX__DEFAULT__PORT", + "value": "5566" + }, { - "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "tcp" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-app-service-configurable-rules", "service": { "ports": [ { - "name": "tcp-48081", + "name": "tcp-48100", "protocol": "TCP", - "port": 48081, - "targetPort": 48081 + "port": 48100, + "targetPort": 48100 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-app-service-configurable-rules" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-app-service-configurable-rules" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-app-service-configurable-rules" } }, "spec": { "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/docker-core-metadata-go:1.3.1", + "name": "edgex-app-service-configurable-rules", + "image": "openyurt/docker-app-service-configurable:1.3.1", "ports": [ { - "name": "tcp-48081", - "containerPort": 48081, + "name": "tcp-48100", + "containerPort": 48100, "protocol": "TCP" } ], @@ -4346,72 +5287,73 @@ ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-metadata" + "name": "MESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-core-data" }, { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "BINDING_PUBLISHTOPIC", + "value": "events" + }, + { + "name": "SERVICE_PORT", + "value": "48100" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-service-configurable-rules" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-app-service-configurable-rules" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-48080", + "name": "tcp-48085", "protocol": "TCP", - "port": 48080, - "targetPort": 48080 + "port": 48085, + "targetPort": 48085 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-support-scheduler" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/docker-core-data-go:1.3.1", + "name": "edgex-support-scheduler", + "image": "openyurt/docker-support-scheduler-go:1.3.1", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-48080", - "containerPort": 48080, + "name": "tcp-48085", + "containerPort": 48085, "protocol": "TCP" } ], @@ -4423,8 +5365,16 @@ } ], "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, { "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", "value": "edgex-core-data" } ], @@ -4432,49 +5382,49 @@ "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-49986", + "name": "tcp-48060", "protocol": "TCP", - "port": 49986, - "targetPort": 49986 + "port": 48060, + "targetPort": 48060 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-support-notifications" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/docker-device-rest-go:1.2.1", + "name": "edgex-support-notifications", + "image": "openyurt/docker-support-notifications-go:1.3.1", "ports": [ { - "name": "tcp-49986", - "containerPort": 49986, + "name": "tcp-48060", + "containerPort": 48060, "protocol": "TCP" } ], @@ -4488,56 +5438,62 @@ "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-support-notifications" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-app-service-configurable-rules", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-48100", + "name": "tcp-6379", "protocol": "TCP", - "port": 48100, - "targetPort": 48100 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-app-service-configurable-rules" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-service-configurable-rules" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-service-configurable-rules" + "app": "edgex-redis" } }, "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-app-service-configurable-rules", - "image": "openyurt/docker-app-service-configurable:1.3.1", + "name": "edgex-redis", + "image": "openyurt/redis:6.0.9-alpine", "ports": [ { - "name": "tcp-48100", - "containerPort": 48100, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], @@ -4548,75 +5504,73 @@ } } ], - "env": [ - { - "name": "MESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-core-data" - }, - { - "name": "SERVICE_PORT", - "value": "48100" - }, - { - "name": "BINDING_PUBLISHTOPIC", - "value": "events" - }, - { - "name": "SERVICE_HOST", - "value": "edgex-app-service-configurable-rules" - }, + "resources": {}, + "volumeMounts": [ { - "name": "EDGEX_PROFILE", - "value": "rules-engine" + "name": "db-data", + "mountPath": "/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-service-configurable-rules" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-48090", + "name": "tcp-8500", "protocol": "TCP", - "port": 48090, - "targetPort": 48090 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/docker-sys-mgmt-agent-go:1.3.1", + "name": "edgex-core-consul", + "image": "openyurt/docker-edgex-consul:1.3.0", "ports": [ { - "name": "tcp-48090", - "containerPort": 48090, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], @@ -4629,23 +5583,33 @@ ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" + "name": "EDGEX_DB", + "value": "redis" }, { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "EDGEX_SECURE", + "value": "false" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" }, { - "name": "METRICSMECHANISM", - "value": "executor" + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-core-consul" } }, "strategy": {} diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/config.json b/pkg/controller/platformadmin/config/EdgeXConfig/config.json index cdd981dd785..55c5b79d57a 100644 --- a/pkg/controller/platformadmin/config/EdgeXConfig/config.json +++ b/pkg/controller/platformadmin/config/EdgeXConfig/config.json @@ -1 +1,11815 @@ -{"versions":[{"versionName":"levski","configMaps":[{"metadata":{"name":"common-variable-levski","creationTimestamp":null},"data":{"API_GATEWAY_HOST":"edgex-kong","API_GATEWAY_STATUS_PORT":"8100","CLIENTS_CORE_COMMAND_HOST":"edgex-core-command","CLIENTS_CORE_DATA_HOST":"edgex-core-data","CLIENTS_CORE_METADATA_HOST":"edgex-core-metadata","CLIENTS_SUPPORT_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_SUPPORT_SCHEDULER_HOST":"edgex-support-scheduler","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","MESSAGEQUEUE_HOST":"edgex-redis","PROXY_SETUP_HOST":"edgex-security-proxy-setup","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_PORT":"8200","SPIFFE_ENDPOINTSOCKET":"/tmp/edgex/secrets/spiffe/public/api.sock","SPIFFE_TRUSTBUNDLE_PATH":"/tmp/edgex/secrets/spiffe/trust/bundle","SPIFFE_TRUSTDOMAIN":"edgexfoundry.org","STAGEGATE_BOOTSTRAPPER_HOST":"edgex-security-bootstrapper","STAGEGATE_BOOTSTRAPPER_STARTPORT":"54321","STAGEGATE_DATABASE_HOST":"edgex-redis","STAGEGATE_DATABASE_PORT":"6379","STAGEGATE_DATABASE_READYPORT":"6379","STAGEGATE_KONGDB_HOST":"edgex-kong-db","STAGEGATE_KONGDB_PORT":"5432","STAGEGATE_KONGDB_READYPORT":"54325","STAGEGATE_READY_TORUNPORT":"54329","STAGEGATE_REGISTRY_HOST":"edgex-core-consul","STAGEGATE_REGISTRY_PORT":"8500","STAGEGATE_REGISTRY_READYPORT":"54324","STAGEGATE_SECRETSTORESETUP_HOST":"edgex-security-secretstore-setup","STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT":"54322","STAGEGATE_WAITFOR_TIMEOUT":"60s"}}],"components":[{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-59882","protocol":"TCP","port":59882,"targetPort":59882}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/core-command:2.3.0","ports":[{"name":"tcp-59882","containerPort":59882,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"MESSAGEQUEUE_EXTERNAL_URL","value":"tcp://edgex-mqtt-broker:1883"},{"name":"SERVICE_HOST","value":"edgex-core-command"},{"name":"MESSAGEQUEUE_INTERNAL_HOST","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-app-rules-engine","service":{"ports":[{"name":"tcp-59701","protocol":"TCP","port":59701,"targetPort":59701}],"selector":{"app":"edgex-app-rules-engine"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-rules-engine"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-rules-engine"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/app-rules-engine","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-app-rules-engine","image":"openyurt/app-service-configurable:2.3.0","ports":[{"name":"tcp-59701","containerPort":59701,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"EDGEX_PROFILE","value":"rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST","value":"edgex-redis"},{"name":"SERVICE_HOST","value":"edgex-app-rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/app-rules-engine"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-rules-engine"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-59860","protocol":"TCP","port":59860,"targetPort":59860}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/support-notifications:2.3.0","ports":[{"name":"tcp-59860","containerPort":59860,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-59861","protocol":"TCP","port":59861,"targetPort":59861}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/support-scheduler:2.3.0","ports":[{"name":"tcp-59861","containerPort":59861,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-scheduler"},{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-59881","protocol":"TCP","port":59881,"targetPort":59881}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/core-metadata:2.3.0","ports":[{"name":"tcp-59881","containerPort":59881,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"},{"name":"SERVICE_HOST","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-59720","protocol":"TCP","port":59720,"targetPort":59720}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"kuiper-data","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}}],"containers":[{"name":"edgex-kuiper","image":"openyurt/ekuiper:1.7.1-alpine","ports":[{"name":"tcp-59720","containerPort":59720,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"EDGEX__DEFAULT__TYPE","value":"redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__SERVER","value":"edgex-redis"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"redis"},{"name":"KUIPER__BASIC__RESTPORT","value":"59720"},{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL","value":"redis"},{"name":"EDGEX__DEFAULT__TOPIC","value":"rules-events"},{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PORT","value":"6379"},{"name":"EDGEX__DEFAULT__PORT","value":"6379"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__TYPE","value":"redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"kuiper-data","mountPath":"/kuiper/data"},{"name":"kuiper-connections","mountPath":"/kuiper/etc/connections"},{"name":"kuiper-sources","mountPath":"/kuiper/etc/sources"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-security-secretstore-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-secretstore-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-secretstore-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"kong","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-security-secretstore-setup","image":"openyurt/security-secretstore-setup:2.3.0","envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"EDGEX_GROUP","value":"2001"},{"name":"SECUREMESSAGEBUS_TYPE","value":"redis"},{"name":"ADD_KNOWN_SECRETS","value":"redisdb[app-rules-engine],redisdb[device-rest],message-bus[device-rest],redisdb[device-virtual],message-bus[device-virtual]"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_SECRETSTORE_TOKENS"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"kong","mountPath":"/tmp/kong"},{"name":"kuiper-sources","mountPath":"/tmp/kuiper"},{"name":"kuiper-connections","mountPath":"/tmp/kuiper-connections"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-secretstore-setup"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-59900","protocol":"TCP","port":59900,"targetPort":59900}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-virtual","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-virtual","image":"openyurt/device-virtual:2.3.0","ports":[{"name":"tcp-59900","containerPort":59900,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-virtual"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-58890","protocol":"TCP","port":58890,"targetPort":58890}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/sys-mgmt-agent","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/sys-mgmt-agent:2.3.0","ports":[{"name":"tcp-58890","containerPort":58890,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"},{"name":"METRICSMECHANISM","value":"executor"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/sys-mgmt-agent"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/consul:1.13.2","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json"},{"name":"ADD_REGISTRY_ACL_ROLES"},{"name":"EDGEX_USER","value":"2002"},{"name":"STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH","value":"/consul/config/consul_acl_done"},{"name":"STAGEGATE_REGISTRY_ACL_MANAGEMENTTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/mgmt_token.json"},{"name":"EDGEX_GROUP","value":"2001"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-consul"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-ui-go","service":{"ports":[{"name":"tcp-4000","protocol":"TCP","port":4000,"targetPort":4000}],"selector":{"app":"edgex-ui-go"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-ui-go"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-ui-go"}},"spec":{"containers":[{"name":"edgex-ui-go","image":"openyurt/edgex-ui:2.3.0","ports":[{"name":"tcp-4000","containerPort":4000,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-ui-go"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-ui-go"}},"strategy":{}}},{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-59986","protocol":"TCP","port":59986,"targetPort":59986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-rest","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-rest","image":"openyurt/device-rest:2.3.0","ports":[{"name":"tcp-59986","containerPort":59986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-rest"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-security-bootstrapper","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrapper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrapper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}}],"containers":[{"name":"edgex-security-bootstrapper","image":"openyurt/security-bootstrapper:2.3.0","envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"EDGEX_USER","value":"2002"},{"name":"EDGEX_GROUP","value":"2001"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrapper"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-59880","protocol":"TCP","port":59880,"targetPort":59880}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/core-data:2.3.0","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-59880","containerPort":59880,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/core-data/secrets-token.json"},{"name":"SERVICE_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"edgex-kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8100","protocol":"TCP","port":8100,"targetPort":8100},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443}],"selector":{"app":"edgex-kong"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}},{"name":"postgres-config","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"edgex-kong","image":"openyurt/kong:2.8.1","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8100","containerPort":8100,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_ADMIN_LISTEN","value":"127.0.0.1:8001, 127.0.0.1:8444 ssl"},{"name":"KONG_PG_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_SSL_CIPHER_SUITE","value":"modern"},{"name":"KONG_STATUS_LISTEN","value":"0.0.0.0:8100"},{"name":"KONG_DNS_VALID_TTL","value":"1"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_DNS_ORDER","value":"LAST,A,CNAME"},{"name":"KONG_PG_HOST","value":"edgex-kong-db"},{"name":"KONG_NGINX_WORKER_PROCESSES","value":"1"},{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong"}},"strategy":{}}},{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"db-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"redis-config","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-bootstrapper-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:7.0.5-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"DATABASECONFIG_NAME","value":"redis.conf"},{"name":"DATABASECONFIG_PATH","value":"/run/redis/conf"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"db-data","mountPath":"/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"redis-config","mountPath":"/run/redis/conf"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-bootstrapper-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"vault-file","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.11.4","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"VAULT_UI","value":"true"},{"name":"VAULT_ADDR","value":"http://edgex-vault:8200"},{"name":"VAULT_CONFIG_DIR","value":"/vault/config"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-security-proxy-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-proxy-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-proxy-setup"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-proxy-setup","image":"openyurt/security-proxy-setup:2.3.0","envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"ROUTES_RULES_ENGINE_HOST","value":"edgex-kuiper"},{"name":"ROUTES_SYS_MGMT_AGENT_HOST","value":"edgex-sys-mgmt-agent"},{"name":"ROUTES_CORE_DATA_HOST","value":"edgex-core-data"},{"name":"ROUTES_CORE_COMMAND_HOST","value":"edgex-core-command"},{"name":"ADD_PROXY_ROUTE"},{"name":"ROUTES_SUPPORT_NOTIFICATIONS_HOST","value":"edgex-support-notifications"},{"name":"ROUTES_CORE_CONSUL_HOST","value":"edgex-core-consul"},{"name":"KONGURL_SERVER","value":"edgex-kong"},{"name":"ROUTES_CORE_METADATA_HOST","value":"edgex-core-metadata"},{"name":"ROUTES_DEVICE_VIRTUAL_HOST","value":"device-virtual"},{"name":"ROUTES_SUPPORT_SCHEDULER_HOST","value":"edgex-support-scheduler"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-proxy-setup"}},"strategy":{}}},{"name":"edgex-kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"edgex-kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"postgres-config","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"edgex-kong-db","image":"openyurt/postgres:13.8-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-levski"}}],"env":[{"name":"POSTGRES_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"POSTGRES_USER","value":"kong"},{"name":"POSTGRES_DB","value":"kong"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong-db"}},"strategy":{}}}]},{"versionName":"jakarta","configMaps":[{"metadata":{"name":"common-variable-jakarta","creationTimestamp":null},"data":{"API_GATEWAY_HOST":"edgex-kong","API_GATEWAY_STATUS_PORT":"8100","CLIENTS_CORE_COMMAND_HOST":"edgex-core-command","CLIENTS_CORE_DATA_HOST":"edgex-core-data","CLIENTS_CORE_METADATA_HOST":"edgex-core-metadata","CLIENTS_SUPPORT_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_SUPPORT_SCHEDULER_HOST":"edgex-support-scheduler","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","MESSAGEQUEUE_HOST":"edgex-redis","PROXY_SETUP_HOST":"edgex-security-proxy-setup","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_PORT":"8200","STAGEGATE_BOOTSTRAPPER_HOST":"edgex-security-bootstrapper","STAGEGATE_BOOTSTRAPPER_STARTPORT":"54321","STAGEGATE_DATABASE_HOST":"edgex-redis","STAGEGATE_DATABASE_PORT":"6379","STAGEGATE_DATABASE_READYPORT":"6379","STAGEGATE_KONGDB_HOST":"edgex-kong-db","STAGEGATE_KONGDB_PORT":"5432","STAGEGATE_KONGDB_READYPORT":"54325","STAGEGATE_READY_TORUNPORT":"54329","STAGEGATE_REGISTRY_HOST":"edgex-core-consul","STAGEGATE_REGISTRY_PORT":"8500","STAGEGATE_REGISTRY_READYPORT":"54324","STAGEGATE_SECRETSTORESETUP_HOST":"edgex-security-secretstore-setup","STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT":"54322","STAGEGATE_WAITFOR_TIMEOUT":"60s"}}],"components":[{"name":"edgex-app-rules-engine","service":{"ports":[{"name":"tcp-59701","protocol":"TCP","port":59701,"targetPort":59701}],"selector":{"app":"edgex-app-rules-engine"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-rules-engine"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-rules-engine"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/app-rules-engine","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-app-rules-engine","image":"openyurt/app-service-configurable:2.1.1","ports":[{"name":"tcp-59701","containerPort":59701,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-app-rules-engine"},{"name":"EDGEX_PROFILE","value":"rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST","value":"edgex-redis"},{"name":"TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/app-rules-engine"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-rules-engine"}},"strategy":{}}},{"name":"edgex-ui-go","service":{"ports":[{"name":"tcp-4000","protocol":"TCP","port":4000,"targetPort":4000}],"selector":{"app":"edgex-ui-go"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-ui-go"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-ui-go"}},"spec":{"containers":[{"name":"edgex-ui-go","image":"openyurt/edgex-ui:2.1.0","ports":[{"name":"tcp-4000","containerPort":4000,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-ui-go"}},"strategy":{}}},{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"db-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"redis-config","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-bootstrapper-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:6.2.6-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"DATABASECONFIG_PATH","value":"/run/redis/conf"},{"name":"DATABASECONFIG_NAME","value":"redis.conf"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"db-data","mountPath":"/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"redis-config","mountPath":"/run/redis/conf"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-bootstrapper-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-58890","protocol":"TCP","port":58890,"targetPort":58890}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/sys-mgmt-agent","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/sys-mgmt-agent:2.1.1","ports":[{"name":"tcp-58890","containerPort":58890,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"METRICSMECHANISM","value":"executor"},{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/sys-mgmt-agent"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-59880","protocol":"TCP","port":59880,"targetPort":59880}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/core-data:2.1.1","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-59880","containerPort":59880,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-data"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/core-data/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-59720","protocol":"TCP","port":59720,"targetPort":59720}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"kuiper-data","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}}],"containers":[{"name":"edgex-kuiper","image":"openyurt/ekuiper:1.4.4-alpine","ports":[{"name":"tcp-59720","containerPort":59720,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"EDGEX__DEFAULT__TYPE","value":"redis"},{"name":"EDGEX__DEFAULT__PORT","value":"6379"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL","value":"redis"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"redis"},{"name":"EDGEX__DEFAULT__TOPIC","value":"rules-events"},{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__SERVER","value":"edgex-redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PORT","value":"6379"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__TYPE","value":"redis"},{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"KUIPER__BASIC__RESTPORT","value":"59720"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"kuiper-data","mountPath":"/kuiper/data"},{"name":"kuiper-connections","mountPath":"/kuiper/etc/connections"},{"name":"kuiper-sources","mountPath":"/kuiper/etc/sources"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-59900","protocol":"TCP","port":59900,"targetPort":59900}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-virtual","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-virtual","image":"openyurt/device-virtual:2.1.1","ports":[{"name":"tcp-59900","containerPort":59900,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-virtual"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-59861","protocol":"TCP","port":59861,"targetPort":59861}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/support-scheduler:2.1.1","ports":[{"name":"tcp-59861","containerPort":59861,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"},{"name":"SERVICE_HOST","value":"edgex-support-scheduler"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-59882","protocol":"TCP","port":59882,"targetPort":59882}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/core-command:2.1.1","ports":[{"name":"tcp-59882","containerPort":59882,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-command"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-59881","protocol":"TCP","port":59881,"targetPort":59881}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/core-metadata:2.1.1","ports":[{"name":"tcp-59881","containerPort":59881,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-metadata"},{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}},{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-59986","protocol":"TCP","port":59986,"targetPort":59986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-rest","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-rest","image":"openyurt/device-rest:2.1.1","ports":[{"name":"tcp-59986","containerPort":59986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-rest"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-security-bootstrapper","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrapper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrapper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}}],"containers":[{"name":"edgex-security-bootstrapper","image":"openyurt/security-bootstrapper:2.1.1","envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"EDGEX_GROUP","value":"2001"},{"name":"EDGEX_USER","value":"2002"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrapper"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-59860","protocol":"TCP","port":59860,"targetPort":59860}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/support-notifications:2.1.1","ports":[{"name":"tcp-59860","containerPort":59860,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"vault-file","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.8.4","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"VAULT_CONFIG_DIR","value":"/vault/config"},{"name":"VAULT_ADDR","value":"http://edgex-vault:8200"},{"name":"VAULT_UI","value":"true"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8100","protocol":"TCP","port":8100,"targetPort":8100},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443}],"selector":{"app":"edgex-kong"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}},{"name":"postgres-config","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"edgex-kong","image":"openyurt/kong:2.5.1","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8100","containerPort":8100,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"KONG_DNS_VALID_TTL","value":"1"},{"name":"KONG_SSL_CIPHER_SUITE","value":"modern"},{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_ADMIN_LISTEN","value":"127.0.0.1:8001, 127.0.0.1:8444 ssl"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_NGINX_WORKER_PROCESSES","value":"1"},{"name":"KONG_DNS_ORDER","value":"LAST,A,CNAME"},{"name":"KONG_PG_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_STATUS_LISTEN","value":"0.0.0.0:8100"},{"name":"KONG_PG_HOST","value":"edgex-kong-db"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong"}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/consul:1.10.3","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json"},{"name":"EDGEX_GROUP","value":"2001"},{"name":"STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH","value":"/consul/config/consul_acl_done"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_REGISTRY_ACL_ROLES"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-consul"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-security-proxy-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-proxy-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-proxy-setup"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-proxy-setup","image":"openyurt/security-proxy-setup:2.1.1","envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"KONGURL_SERVER","value":"edgex-kong"},{"name":"ADD_PROXY_ROUTE"},{"name":"ROUTES_CORE_DATA_HOST","value":"edgex-core-data"},{"name":"ROUTES_RULES_ENGINE_HOST","value":"edgex-kuiper"},{"name":"ROUTES_SUPPORT_NOTIFICATIONS_HOST","value":"edgex-support-notifications"},{"name":"ROUTES_CORE_CONSUL_HOST","value":"edgex-core-consul"},{"name":"ROUTES_SYS_MGMT_AGENT_HOST","value":"edgex-sys-mgmt-agent"},{"name":"ROUTES_DEVICE_VIRTUAL_HOST","value":"device-virtual"},{"name":"ROUTES_SUPPORT_SCHEDULER_HOST","value":"edgex-support-scheduler"},{"name":"ROUTES_CORE_COMMAND_HOST","value":"edgex-core-command"},{"name":"ROUTES_CORE_METADATA_HOST","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-proxy-setup"}},"strategy":{}}},{"name":"edgex-security-secretstore-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-secretstore-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-secretstore-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"kong","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-security-secretstore-setup","image":"openyurt/security-secretstore-setup:2.1.1","envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"EDGEX_GROUP","value":"2001"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_KNOWN_SECRETS","value":"redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]"},{"name":"ADD_SECRETSTORE_TOKENS"},{"name":"SECUREMESSAGEBUS_TYPE","value":"redis"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"kong","mountPath":"/tmp/kong"},{"name":"kuiper-sources","mountPath":"/tmp/kuiper"},{"name":"kuiper-connections","mountPath":"/tmp/kuiper-connections"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-secretstore-setup"}},"strategy":{}}},{"name":"edgex-kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"edgex-kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"postgres-config","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"edgex-kong-db","image":"openyurt/postgres:13.4-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-jakarta"}}],"env":[{"name":"POSTGRES_USER","value":"kong"},{"name":"POSTGRES_DB","value":"kong"},{"name":"POSTGRES_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong-db"}},"strategy":{}}}]},{"versionName":"kamakura","configMaps":[{"metadata":{"name":"common-variable-kamakura","creationTimestamp":null},"data":{"API_GATEWAY_HOST":"edgex-kong","API_GATEWAY_STATUS_PORT":"8100","CLIENTS_CORE_COMMAND_HOST":"edgex-core-command","CLIENTS_CORE_DATA_HOST":"edgex-core-data","CLIENTS_CORE_METADATA_HOST":"edgex-core-metadata","CLIENTS_SUPPORT_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_SUPPORT_SCHEDULER_HOST":"edgex-support-scheduler","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","MESSAGEQUEUE_HOST":"edgex-redis","PROXY_SETUP_HOST":"edgex-security-proxy-setup","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_PORT":"8200","SPIFFE_ENDPOINTSOCKET":"/tmp/edgex/secrets/spiffe/public/api.sock","SPIFFE_TRUSTBUNDLE_PATH":"/tmp/edgex/secrets/spiffe/trust/bundle","SPIFFE_TRUSTDOMAIN":"edgexfoundry.org","STAGEGATE_BOOTSTRAPPER_HOST":"edgex-security-bootstrapper","STAGEGATE_BOOTSTRAPPER_STARTPORT":"54321","STAGEGATE_DATABASE_HOST":"edgex-redis","STAGEGATE_DATABASE_PORT":"6379","STAGEGATE_DATABASE_READYPORT":"6379","STAGEGATE_KONGDB_HOST":"edgex-kong-db","STAGEGATE_KONGDB_PORT":"5432","STAGEGATE_KONGDB_READYPORT":"54325","STAGEGATE_READY_TORUNPORT":"54329","STAGEGATE_REGISTRY_HOST":"edgex-core-consul","STAGEGATE_REGISTRY_PORT":"8500","STAGEGATE_REGISTRY_READYPORT":"54324","STAGEGATE_SECRETSTORESETUP_HOST":"edgex-security-secretstore-setup","STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT":"54322","STAGEGATE_WAITFOR_TIMEOUT":"60s"}}],"components":[{"name":"edgex-security-bootstrapper","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrapper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrapper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}}],"containers":[{"name":"edgex-security-bootstrapper","image":"openyurt/security-bootstrapper:2.2.0","envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"EDGEX_GROUP","value":"2001"},{"name":"EDGEX_USER","value":"2002"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrapper"}},"strategy":{}}},{"name":"edgex-app-rules-engine","service":{"ports":[{"name":"tcp-59701","protocol":"TCP","port":59701,"targetPort":59701}],"selector":{"app":"edgex-app-rules-engine"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-rules-engine"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-rules-engine"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/app-rules-engine","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-app-rules-engine","image":"openyurt/app-service-configurable:2.2.0","ports":[{"name":"tcp-59701","containerPort":59701,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-redis"},{"name":"TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST","value":"edgex-redis"},{"name":"EDGEX_PROFILE","value":"rules-engine"},{"name":"SERVICE_HOST","value":"edgex-app-rules-engine"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/app-rules-engine"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-rules-engine"}},"strategy":{}}},{"name":"edgex-security-proxy-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-proxy-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-proxy-setup"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-proxy-setup","image":"openyurt/security-proxy-setup:2.2.0","envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"ROUTES_RULES_ENGINE_HOST","value":"edgex-kuiper"},{"name":"KONGURL_SERVER","value":"edgex-kong"},{"name":"ROUTES_DEVICE_VIRTUAL_HOST","value":"device-virtual"},{"name":"ROUTES_CORE_DATA_HOST","value":"edgex-core-data"},{"name":"ROUTES_CORE_COMMAND_HOST","value":"edgex-core-command"},{"name":"ADD_PROXY_ROUTE"},{"name":"ROUTES_SYS_MGMT_AGENT_HOST","value":"edgex-sys-mgmt-agent"},{"name":"ROUTES_SUPPORT_NOTIFICATIONS_HOST","value":"edgex-support-notifications"},{"name":"ROUTES_CORE_METADATA_HOST","value":"edgex-core-metadata"},{"name":"ROUTES_CORE_CONSUL_HOST","value":"edgex-core-consul"},{"name":"ROUTES_SUPPORT_SCHEDULER_HOST","value":"edgex-support-scheduler"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-proxy-setup"}},"strategy":{}}},{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-59986","protocol":"TCP","port":59986,"targetPort":59986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-rest","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-rest","image":"openyurt/device-rest:2.2.0","ports":[{"name":"tcp-59986","containerPort":59986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-rest"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-59720","protocol":"TCP","port":59720,"targetPort":59720}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"kuiper-data","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}}],"containers":[{"name":"edgex-kuiper","image":"openyurt/ekuiper:1.4.4-alpine","ports":[{"name":"tcp-59720","containerPort":59720,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"EDGEX__DEFAULT__TOPIC","value":"rules-events"},{"name":"EDGEX__DEFAULT__PORT","value":"6379"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PORT","value":"6379"},{"name":"KUIPER__BASIC__RESTPORT","value":"59720"},{"name":"EDGEX__DEFAULT__TYPE","value":"redis"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL","value":"redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__SERVER","value":"edgex-redis"},{"name":"CONNECTION__EDGEX__REDISMSGBUS__TYPE","value":"redis"},{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"kuiper-data","mountPath":"/kuiper/data"},{"name":"kuiper-connections","mountPath":"/kuiper/etc/connections"},{"name":"kuiper-sources","mountPath":"/kuiper/etc/sources"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-58890","protocol":"TCP","port":58890,"targetPort":58890}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/sys-mgmt-agent","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/sys-mgmt-agent:2.2.0","ports":[{"name":"tcp-58890","containerPort":58890,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"},{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"METRICSMECHANISM","value":"executor"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/sys-mgmt-agent"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-59881","protocol":"TCP","port":59881,"targetPort":59881}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/core-metadata:2.2.0","ports":[{"name":"tcp-59881","containerPort":59881,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-metadata"},{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-59880","protocol":"TCP","port":59880,"targetPort":59880}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/core-data:2.2.0","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-59880","containerPort":59880,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/core-data/secrets-token.json"},{"name":"SERVICE_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"edgex-security-secretstore-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-secretstore-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-secretstore-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"kong","emptyDir":{}},{"name":"kuiper-sources","emptyDir":{}},{"name":"kuiper-connections","emptyDir":{}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-security-secretstore-setup","image":"openyurt/security-secretstore-setup:2.2.0","envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"ADD_SECRETSTORE_TOKENS"},{"name":"EDGEX_GROUP","value":"2001"},{"name":"SECUREMESSAGEBUS_TYPE","value":"redis"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_KNOWN_SECRETS","value":"redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"kong","mountPath":"/tmp/kong"},{"name":"kuiper-sources","mountPath":"/tmp/kuiper"},{"name":"kuiper-connections","mountPath":"/tmp/kuiper-connections"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-secretstore-setup"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-59900","protocol":"TCP","port":59900,"targetPort":59900}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-virtual","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-virtual","image":"openyurt/device-virtual:2.2.0","ports":[{"name":"tcp-59900","containerPort":59900,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-virtual"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"edgex-ui-go","service":{"ports":[{"name":"tcp-4000","protocol":"TCP","port":4000,"targetPort":4000}],"selector":{"app":"edgex-ui-go"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-ui-go"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-ui-go"}},"spec":{"containers":[{"name":"edgex-ui-go","image":"openyurt/edgex-ui:2.2.0","ports":[{"name":"tcp-4000","containerPort":4000,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-ui-go"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"vault-file","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.8.9","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"VAULT_CONFIG_DIR","value":"/vault/config"},{"name":"VAULT_ADDR","value":"http://edgex-vault:8200"},{"name":"VAULT_UI","value":"true"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-59882","protocol":"TCP","port":59882,"targetPort":59882}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/core-command:2.2.0","ports":[{"name":"tcp-59882","containerPort":59882,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-command"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/consul:1.10.10","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"ADD_REGISTRY_ACL_ROLES"},{"name":"STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH","value":"/consul/config/consul_acl_done"},{"name":"STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json"},{"name":"EDGEX_USER","value":"2002"},{"name":"EDGEX_GROUP","value":"2001"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-consul"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"edgex-kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"postgres-config","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"edgex-kong-db","image":"openyurt/postgres:13.5-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"POSTGRES_USER","value":"kong"},{"name":"POSTGRES_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"POSTGRES_DB","value":"kong"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong-db"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-59861","protocol":"TCP","port":59861,"targetPort":59861}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/support-scheduler:2.2.0","ports":[{"name":"tcp-59861","containerPort":59861,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"SERVICE_HOST","value":"edgex-support-scheduler"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"db-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"redis-config","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-bootstrapper-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:6.2.6-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"DATABASECONFIG_NAME","value":"redis.conf"},{"name":"DATABASECONFIG_PATH","value":"/run/redis/conf"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"db-data","mountPath":"/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"redis-config","mountPath":"/run/redis/conf"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-bootstrapper-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-59860","protocol":"TCP","port":59860,"targetPort":59860}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/support-notifications:2.2.0","ports":[{"name":"tcp-59860","containerPort":59860,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}},{"name":"edgex-kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8100","protocol":"TCP","port":8100,"targetPort":8100},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443}],"selector":{"app":"edgex-kong"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}},{"name":"postgres-config","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"edgex-kong","image":"openyurt/kong:2.6.1","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8100","containerPort":8100,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-kamakura"}}],"env":[{"name":"KONG_NGINX_WORKER_PROCESSES","value":"1"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_PG_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"KONG_STATUS_LISTEN","value":"0.0.0.0:8100"},{"name":"KONG_SSL_CIPHER_SUITE","value":"modern"},{"name":"KONG_DNS_ORDER","value":"LAST,A,CNAME"},{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_DNS_VALID_TTL","value":"1"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_PG_HOST","value":"edgex-kong-db"},{"name":"KONG_ADMIN_LISTEN","value":"127.0.0.1:8001, 127.0.0.1:8444 ssl"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong"}},"strategy":{}}}]},{"versionName":"ireland","configMaps":[{"metadata":{"name":"common-variable-ireland","creationTimestamp":null},"data":{"API_GATEWAY_HOST":"edgex-kong","API_GATEWAY_STATUS_PORT":"8100","CLIENTS_CORE_COMMAND_HOST":"edgex-core-command","CLIENTS_CORE_DATA_HOST":"edgex-core-data","CLIENTS_CORE_METADATA_HOST":"edgex-core-metadata","CLIENTS_SUPPORT_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_SUPPORT_SCHEDULER_HOST":"edgex-support-scheduler","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","MESSAGEQUEUE_HOST":"edgex-redis","PROXY_SETUP_HOST":"edgex-security-proxy-setup","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_PORT":"8200","STAGEGATE_BOOTSTRAPPER_HOST":"edgex-security-bootstrapper","STAGEGATE_BOOTSTRAPPER_STARTPORT":"54321","STAGEGATE_DATABASE_HOST":"edgex-redis","STAGEGATE_DATABASE_PORT":"6379","STAGEGATE_DATABASE_READYPORT":"6379","STAGEGATE_KONGDB_HOST":"edgex-kong-db","STAGEGATE_KONGDB_PORT":"5432","STAGEGATE_KONGDB_READYPORT":"54325","STAGEGATE_READY_TORUNPORT":"54329","STAGEGATE_REGISTRY_HOST":"edgex-core-consul","STAGEGATE_REGISTRY_PORT":"8500","STAGEGATE_REGISTRY_READYPORT":"54324","STAGEGATE_SECRETSTORESETUP_HOST":"edgex-security-secretstore-setup","STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT":"54322","STAGEGATE_WAITFOR_TIMEOUT":"60s"}}],"components":[{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"db-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"redis-config","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-bootstrapper-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:6.2.4-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"DATABASECONFIG_NAME","value":"redis.conf"},{"name":"DATABASECONFIG_PATH","value":"/run/redis/conf"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"db-data","mountPath":"/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"redis-config","mountPath":"/run/redis/conf"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-bootstrapper-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-59900","protocol":"TCP","port":59900,"targetPort":59900}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-virtual","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-virtual","image":"openyurt/device-virtual:2.0.0","ports":[{"name":"tcp-59900","containerPort":59900,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-virtual"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-59881","protocol":"TCP","port":59881,"targetPort":59881}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/core-metadata:2.0.0","ports":[{"name":"tcp-59881","containerPort":59881,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-metadata"},{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}},{"name":"edgex-app-rules-engine","service":{"ports":[{"name":"tcp-59701","protocol":"TCP","port":59701,"targetPort":59701}],"selector":{"app":"edgex-app-rules-engine"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-rules-engine"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-rules-engine"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/app-rules-engine","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-app-rules-engine","image":"openyurt/app-service-configurable:2.0.1","ports":[{"name":"tcp-59701","containerPort":59701,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"EDGEX_PROFILE","value":"rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-redis"},{"name":"SERVICE_HOST","value":"edgex-app-rules-engine"},{"name":"TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST","value":"edgex-redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/app-rules-engine"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-rules-engine"}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/consul:1.9.5","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH","value":"/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json"},{"name":"EDGEX_GROUP","value":"2001"},{"name":"ADD_REGISTRY_ACL_ROLES"},{"name":"STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH","value":"/consul/config/consul_acl_done"},{"name":"EDGEX_USER","value":"2002"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-consul"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-59986","protocol":"TCP","port":59986,"targetPort":59986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/device-rest","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-device-rest","image":"openyurt/device-rest:2.0.0","ports":[{"name":"tcp-59986","containerPort":59986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/device-rest"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"vault-file","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.7.2","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"VAULT_ADDR","value":"http://edgex-vault:8200"},{"name":"VAULT_UI","value":"true"},{"name":"VAULT_CONFIG_DIR","value":"/vault/config"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-59882","protocol":"TCP","port":59882,"targetPort":59882}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/core-command:2.0.0","ports":[{"name":"tcp-59882","containerPort":59882,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-command"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-59880","protocol":"TCP","port":59880,"targetPort":59880}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/core-data:2.0.0","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-59880","containerPort":59880,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-data"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/core-data/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-59720","protocol":"TCP","port":59720,"targetPort":59720}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"kuiper-data","emptyDir":{}},{"name":"kuiper-config","emptyDir":{}}],"containers":[{"name":"edgex-kuiper","image":"openyurt/ekuiper:1.3.0-alpine","ports":[{"name":"tcp-59720","containerPort":59720,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-redis"},{"name":"EDGEX__DEFAULT__TYPE","value":"redis"},{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"EDGEX__DEFAULT__TOPIC","value":"rules-events"},{"name":"EDGEX__DEFAULT__PORT","value":"6379"},{"name":"KUIPER__BASIC__RESTPORT","value":"59720"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"redis"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"kuiper-data","mountPath":"/kuiper/data"},{"name":"kuiper-config","mountPath":"/kuiper/etc/sources"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-security-proxy-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-proxy-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-proxy-setup"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"consul-acl-token","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-proxy-setup","image":"openyurt/security-proxy-setup:2.0.0","envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"ROUTES_SYS_MGMT_AGENT_HOST","value":"edgex-sys-mgmt-agent"},{"name":"ROUTES_CORE_METADATA_HOST","value":"edgex-core-metadata"},{"name":"ROUTES_DEVICE_VIRTUAL_HOST","value":"device-virtual"},{"name":"ROUTES_RULES_ENGINE_HOST","value":"edgex-kuiper"},{"name":"ROUTES_SUPPORT_SCHEDULER_HOST","value":"edgex-support-scheduler"},{"name":"KONGURL_SERVER","value":"edgex-kong"},{"name":"ROUTES_CORE_DATA_HOST","value":"edgex-core-data"},{"name":"ROUTES_SUPPORT_NOTIFICATIONS_HOST","value":"edgex-support-notifications"},{"name":"ADD_PROXY_ROUTE"},{"name":"ROUTES_CORE_COMMAND_HOST","value":"edgex-core-command"},{"name":"ROUTES_CORE_CONSUL_HOST","value":"edgex-core-consul"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"consul-acl-token","mountPath":"/tmp/edgex/secrets/consul-acl-token"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-proxy-setup"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-59861","protocol":"TCP","port":59861,"targetPort":59861}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/support-scheduler:2.0.0","ports":[{"name":"tcp-59861","containerPort":59861,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"SERVICE_HOST","value":"edgex-support-scheduler"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8100","protocol":"TCP","port":8100,"targetPort":8100},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443}],"selector":{"app":"edgex-kong"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/security-proxy-setup","type":"DirectoryOrCreate"}},{"name":"postgres-config","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"edgex-kong","image":"openyurt/kong:2.4.1-alpine","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8100","containerPort":8100,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_DNS_VALID_TTL","value":"1"},{"name":"KONG_STATUS_LISTEN","value":"0.0.0.0:8100"},{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_DNS_ORDER","value":"LAST,A,CNAME"},{"name":"KONG_PG_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"KONG_PG_HOST","value":"edgex-kong-db"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_ADMIN_LISTEN","value":"127.0.0.1:8001, 127.0.0.1:8444 ssl"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/security-proxy-setup"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong"}},"strategy":{}}},{"name":"edgex-security-secretstore-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-security-secretstore-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-secretstore-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"kong","emptyDir":{}},{"name":"kuiper-config","emptyDir":{}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-security-secretstore-setup","image":"openyurt/security-secretstore-setup:2.0.0","envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"ADD_KNOWN_SECRETS","value":"redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]"},{"name":"EDGEX_GROUP","value":"2001"},{"name":"EDGEX_USER","value":"2002"},{"name":"ADD_SECRETSTORE_TOKENS"},{"name":"SECUREMESSAGEBUS_TYPE","value":"redis"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"kong","mountPath":"/tmp/kong"},{"name":"kuiper-config","mountPath":"/tmp/kuiper"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-secretstore-setup"}},"strategy":{}}},{"name":"edgex-kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"edgex-kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"edgex-init","emptyDir":{}},{"name":"postgres-config","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"edgex-kong-db","image":"openyurt/postgres:12.3-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"POSTGRES_DB","value":"kong"},{"name":"POSTGRES_PASSWORD_FILE","value":"/tmp/postgres-config/.pgpassword"},{"name":"POSTGRES_USER","value":"kong"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"postgres-config","mountPath":"/tmp/postgres-config"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kong-db"}},"strategy":{}}},{"name":"edgex-security-bootstrapper","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrapper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrapper"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}}],"containers":[{"name":"edgex-security-bootstrapper","image":"openyurt/security-bootstrapper:2.0.0","envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"EDGEX_USER","value":"2002"},{"name":"EDGEX_GROUP","value":"2001"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrapper"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-58890","protocol":"TCP","port":58890,"targetPort":58890}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/sys-mgmt-agent","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/sys-mgmt-agent:2.0.0","ports":[{"name":"tcp-58890","containerPort":58890,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"},{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"METRICSMECHANISM","value":"executor"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/sys-mgmt-agent"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-59860","protocol":"TCP","port":59860,"targetPort":59860}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"edgex-init","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/support-notifications:2.0.0","ports":[{"name":"tcp-59860","containerPort":59860,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-ireland"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"edgex-init","mountPath":"/edgex-init"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}}]},{"versionName":"hanoi","configMaps":[{"metadata":{"name":"common-variable-hanoi","creationTimestamp":null},"data":{"CLIENTS_COMMAND_HOST":"edgex-core-command","CLIENTS_COREDATA_HOST":"edgex-core-data","CLIENTS_DATA_HOST":"edgex-core-data","CLIENTS_METADATA_HOST":"edgex-core-metadata","CLIENTS_NOTIFICATIONS_HOST":"edgex-support-notifications","CLIENTS_RULESENGINE_HOST":"edgex-kuiper","CLIENTS_SCHEDULER_HOST":"edgex-support-scheduler","CLIENTS_VIRTUALDEVICE_HOST":"edgex-device-virtual","DATABASES_PRIMARY_HOST":"edgex-redis","EDGEX_SECURITY_SECRET_STORE":"true","LOGGING_ENABLEREMOTE":"false","REGISTRY_HOST":"edgex-core-consul","SECRETSTORE_HOST":"edgex-vault","SECRETSTORE_ROOTCACERTPATH":"/tmp/edgex/secrets/ca/ca.pem","SECRETSTORE_SERVERNAME":"edgex-vault","SERVICE_SERVERBINDADDR":"0.0.0.0"}}],"components":[{"name":"edgex-device-rest","service":{"ports":[{"name":"tcp-49986","protocol":"TCP","port":49986,"targetPort":49986}],"selector":{"app":"edgex-device-rest"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-rest"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-rest"}},"spec":{"containers":[{"name":"edgex-device-rest","image":"openyurt/docker-device-rest-go:1.2.1","ports":[{"name":"tcp-49986","containerPort":49986,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-rest"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-rest"}},"strategy":{}}},{"name":"edgex-core-command","service":{"ports":[{"name":"tcp-48082","protocol":"TCP","port":48082,"targetPort":48082}],"selector":{"app":"edgex-core-command"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-command"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-command"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-core-command","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-command","image":"openyurt/docker-core-command-go:1.3.1","ports":[{"name":"tcp-48082","containerPort":48082,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-command"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-core-command/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-core-command"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-command"}},"strategy":{}}},{"name":"edgex-support-notifications","service":{"ports":[{"name":"tcp-48060","protocol":"TCP","port":48060,"targetPort":48060}],"selector":{"app":"edgex-support-notifications"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-notifications"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-notifications"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-support-notifications","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-notifications","image":"openyurt/docker-support-notifications-go:1.3.1","ports":[{"name":"tcp-48060","containerPort":48060,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-support-notifications/secrets-token.json"},{"name":"SERVICE_HOST","value":"edgex-support-notifications"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-support-notifications"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-notifications"}},"strategy":{}}},{"name":"edgex-vault-worker","deployment":{"selector":{"matchLabels":{"app":"edgex-vault-worker"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault-worker"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"consul-scripts","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"vault-config","emptyDir":{}}],"containers":[{"name":"edgex-vault-worker","image":"openyurt/docker-security-secretstore-setup-go:1.3.1","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSTORE_SETUP_DONE_FLAG","value":"/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"consul-scripts","mountPath":"/consul/scripts"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"vault-config","mountPath":"/vault/config"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault-worker"}},"strategy":{}}},{"name":"edgex-support-scheduler","service":{"ports":[{"name":"tcp-48085","protocol":"TCP","port":48085,"targetPort":48085}],"selector":{"app":"edgex-support-scheduler"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-support-scheduler"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-support-scheduler"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-support-scheduler","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-support-scheduler","image":"openyurt/docker-support-scheduler-go:1.3.1","ports":[{"name":"tcp-48085","containerPort":48085,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-support-scheduler/secrets-token.json"},{"name":"INTERVALACTIONS_SCRUBAGED_HOST","value":"edgex-core-data"},{"name":"INTERVALACTIONS_SCRUBPUSHED_HOST","value":"edgex-core-data"},{"name":"SERVICE_HOST","value":"edgex-support-scheduler"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-support-scheduler"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-support-scheduler"}},"strategy":{}}},{"name":"edgex-secrets-setup","deployment":{"selector":{"matchLabels":{"app":"edgex-secrets-setup"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-secrets-setup"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"secrets-setup-cache","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets","type":"DirectoryOrCreate"}},{"name":"vault-init","emptyDir":{}}],"containers":[{"name":"edgex-secrets-setup","image":"openyurt/docker-security-secrets-setup-go:1.3.1","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/tmp"},{"name":"tmpfs-volume2","mountPath":"/run"},{"name":"secrets-setup-cache","mountPath":"/etc/edgex/pki"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets"},{"name":"vault-init","mountPath":"/vault/init"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-secrets-setup"}},"strategy":{}}},{"name":"edgex-vault","service":{"ports":[{"name":"tcp-8200","protocol":"TCP","port":8200,"targetPort":8200}],"selector":{"app":"edgex-vault"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-vault"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-vault"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/edgex-vault","type":"DirectoryOrCreate"}},{"name":"vault-file","emptyDir":{}},{"name":"vault-init","emptyDir":{}},{"name":"vault-logs","emptyDir":{}}],"containers":[{"name":"edgex-vault","image":"openyurt/vault:1.5.3","ports":[{"name":"tcp-8200","containerPort":8200,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"VAULT_CONFIG_DIR","value":"/vault/config"},{"name":"VAULT_UI","value":"true"},{"name":"VAULT_ADDR","value":"https://edgex-vault:8200"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/vault/config"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/edgex-vault"},{"name":"vault-file","mountPath":"/vault/file"},{"name":"vault-init","mountPath":"/vault/init"},{"name":"vault-logs","mountPath":"/vault/logs"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-vault"}},"strategy":{}}},{"name":"edgex-core-data","service":{"ports":[{"name":"tcp-5563","protocol":"TCP","port":5563,"targetPort":5563},{"name":"tcp-48080","protocol":"TCP","port":48080,"targetPort":48080}],"selector":{"app":"edgex-core-data"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-data"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-data"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-core-data","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-data","image":"openyurt/docker-core-data-go:1.3.1","ports":[{"name":"tcp-5563","containerPort":5563,"protocol":"TCP"},{"name":"tcp-48080","containerPort":48080,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-data"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-core-data/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-core-data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-data"}},"strategy":{}}},{"name":"kong","service":{"ports":[{"name":"tcp-8000","protocol":"TCP","port":8000,"targetPort":8000},{"name":"tcp-8001","protocol":"TCP","port":8001,"targetPort":8001},{"name":"tcp-8443","protocol":"TCP","port":8443,"targetPort":8443},{"name":"tcp-8444","protocol":"TCP","port":8444,"targetPort":8444}],"selector":{"app":"kong"}},"deployment":{"selector":{"matchLabels":{"app":"kong"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"kong"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"consul-scripts","emptyDir":{}},{"name":"kong","emptyDir":{}}],"containers":[{"name":"kong","image":"openyurt/kong:2.0.5","ports":[{"name":"tcp-8000","containerPort":8000,"protocol":"TCP"},{"name":"tcp-8001","containerPort":8001,"protocol":"TCP"},{"name":"tcp-8443","containerPort":8443,"protocol":"TCP"},{"name":"tcp-8444","containerPort":8444,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"KONG_ADMIN_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_ADMIN_ERROR_LOG","value":"/dev/stderr"},{"name":"KONG_ADMIN_LISTEN","value":"0.0.0.0:8001, 0.0.0.0:8444 ssl"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_PG_HOST","value":"kong-db"},{"name":"KONG_PG_PASSWORD","value":"kong"},{"name":"KONG_PROXY_ACCESS_LOG","value":"/dev/stdout"},{"name":"KONG_PROXY_ERROR_LOG","value":"/dev/stderr"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"consul-scripts","mountPath":"/consul/scripts"},{"name":"kong","mountPath":"/usr/local/kong"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"kong"}},"strategy":{}}},{"name":"","deployment":{"selector":{"matchLabels":{"app":""}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":""}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"consul-scripts","emptyDir":{}}],"containers":[{"name":"","image":"openyurt/kong:2.0.5","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"KONG_PG_PASSWORD","value":"kong"},{"name":"KONG_DATABASE","value":"postgres"},{"name":"KONG_PG_HOST","value":"kong-db"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/tmp"},{"name":"consul-scripts","mountPath":"/consul/scripts"}],"imagePullPolicy":"IfNotPresent"}]}},"strategy":{}}},{"name":"edgex-core-consul","service":{"ports":[{"name":"tcp-8500","protocol":"TCP","port":8500,"targetPort":8500}],"selector":{"app":"edgex-core-consul"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-consul"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-consul"}},"spec":{"volumes":[{"name":"consul-config","emptyDir":{}},{"name":"consul-data","emptyDir":{}},{"name":"consul-scripts","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-consul","type":"DirectoryOrCreate"}},{"name":"anonymous-volume3","hostPath":{"path":"/tmp/edgex/secrets/edgex-kong","type":"DirectoryOrCreate"}},{"name":"anonymous-volume4","hostPath":{"path":"/tmp/edgex/secrets/edgex-vault","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-consul","image":"openyurt/docker-edgex-consul:1.3.0","ports":[{"name":"tcp-8500","containerPort":8500,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSTORE_SETUP_DONE_FLAG","value":"/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done"},{"name":"EDGEX_DB","value":"redis"},{"name":"EDGEX_SECURE","value":"true"}],"resources":{},"volumeMounts":[{"name":"consul-config","mountPath":"/consul/config"},{"name":"consul-data","mountPath":"/consul/data"},{"name":"consul-scripts","mountPath":"/consul/scripts"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-consul"},{"name":"anonymous-volume3","mountPath":"/tmp/edgex/secrets/edgex-kong"},{"name":"anonymous-volume4","mountPath":"/tmp/edgex/secrets/edgex-vault"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-consul"}},"strategy":{}}},{"name":"edgex-security-bootstrap-database","deployment":{"selector":{"matchLabels":{"app":"edgex-security-bootstrap-database"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-security-bootstrap-database"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-security-bootstrap-redis","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-security-bootstrap-database","image":"openyurt/docker-security-bootstrap-redis-go:1.3.1","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-security-bootstrap-database"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-security-bootstrap-redis/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/run"},{"name":"tmpfs-volume2","mountPath":"/vault"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-security-bootstrap-redis"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-security-bootstrap-database"}},"strategy":{}}},{"name":"edgex-redis","service":{"ports":[{"name":"tcp-6379","protocol":"TCP","port":6379,"targetPort":6379}],"selector":{"app":"edgex-redis"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-redis"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-redis"}},"spec":{"volumes":[{"name":"db-data","emptyDir":{}}],"containers":[{"name":"edgex-redis","image":"openyurt/redis:6.0.9-alpine","ports":[{"name":"tcp-6379","containerPort":6379,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"resources":{},"volumeMounts":[{"name":"db-data","mountPath":"/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-redis"}},"strategy":{}}},{"name":"edgex-device-virtual","service":{"ports":[{"name":"tcp-49990","protocol":"TCP","port":49990,"targetPort":49990}],"selector":{"app":"edgex-device-virtual"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-device-virtual"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-device-virtual"}},"spec":{"containers":[{"name":"edgex-device-virtual","image":"openyurt/docker-device-virtual-go:1.3.1","ports":[{"name":"tcp-49990","containerPort":49990,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-device-virtual"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-device-virtual"}},"strategy":{}}},{"name":"kong-db","service":{"ports":[{"name":"tcp-5432","protocol":"TCP","port":5432,"targetPort":5432}],"selector":{"app":"kong-db"}},"deployment":{"selector":{"matchLabels":{"app":"kong-db"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"kong-db"}},"spec":{"volumes":[{"name":"tmpfs-volume1","emptyDir":{}},{"name":"tmpfs-volume2","emptyDir":{}},{"name":"tmpfs-volume3","emptyDir":{}},{"name":"postgres-data","emptyDir":{}}],"containers":[{"name":"kong-db","image":"openyurt/postgres:12.3-alpine","ports":[{"name":"tcp-5432","containerPort":5432,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"POSTGRES_DB","value":"kong"},{"name":"POSTGRES_PASSWORD","value":"kong"},{"name":"POSTGRES_USER","value":"kong"}],"resources":{},"volumeMounts":[{"name":"tmpfs-volume1","mountPath":"/var/run"},{"name":"tmpfs-volume2","mountPath":"/tmp"},{"name":"tmpfs-volume3","mountPath":"/run"},{"name":"postgres-data","mountPath":"/var/lib/postgresql/data"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"kong-db"}},"strategy":{}}},{"name":"edgex-sys-mgmt-agent","service":{"ports":[{"name":"tcp-48090","protocol":"TCP","port":48090,"targetPort":48090}],"selector":{"app":"edgex-sys-mgmt-agent"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-sys-mgmt-agent"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-sys-mgmt-agent"}},"spec":{"containers":[{"name":"edgex-sys-mgmt-agent","image":"openyurt/docker-sys-mgmt-agent-go:1.3.1","ports":[{"name":"tcp-48090","containerPort":48090,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-sys-mgmt-agent"},{"name":"EXECUTORPATH","value":"/sys-mgmt-executor"},{"name":"METRICSMECHANISM","value":"executor"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-sys-mgmt-agent"}},"strategy":{}}},{"name":"edgex-kuiper","service":{"ports":[{"name":"tcp-20498","protocol":"TCP","port":20498,"targetPort":20498},{"name":"tcp-48075","protocol":"TCP","port":48075,"targetPort":48075}],"selector":{"app":"edgex-kuiper"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-kuiper"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-kuiper"}},"spec":{"containers":[{"name":"edgex-kuiper","image":"openyurt/kuiper:1.1.1-alpine","ports":[{"name":"tcp-20498","containerPort":20498,"protocol":"TCP"},{"name":"tcp-48075","containerPort":48075,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"EDGEX__DEFAULT__SERVICESERVER","value":"http://edgex-core-data:48080"},{"name":"EDGEX__DEFAULT__TOPIC","value":"events"},{"name":"KUIPER__BASIC__CONSOLELOG","value":"true"},{"name":"KUIPER__BASIC__RESTPORT","value":"48075"},{"name":"EDGEX__DEFAULT__PORT","value":"5566"},{"name":"EDGEX__DEFAULT__PROTOCOL","value":"tcp"},{"name":"EDGEX__DEFAULT__SERVER","value":"edgex-app-service-configurable-rules"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-kuiper"}},"strategy":{}}},{"name":"edgex-proxy","deployment":{"selector":{"matchLabels":{"app":"edgex-proxy"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-proxy"}},"spec":{"volumes":[{"name":"consul-scripts","emptyDir":{}},{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-security-proxy-setup","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-proxy","image":"openyurt/docker-security-proxy-setup-go:1.3.1","envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SECRETSERVICE_SERVER","value":"edgex-vault"},{"name":"KONGURL_SERVER","value":"kong"},{"name":"SECRETSERVICE_SNIS","value":"edgex-kong"},{"name":"SECRETSERVICE_CACERTPATH","value":"/tmp/edgex/secrets/ca/ca.pem"},{"name":"SECRETSERVICE_TOKENPATH","value":"/tmp/edgex/secrets/edgex-security-proxy-setup/secrets-token.json"}],"resources":{},"volumeMounts":[{"name":"consul-scripts","mountPath":"/consul/scripts"},{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-security-proxy-setup"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-proxy"}},"strategy":{}}},{"name":"edgex-app-service-configurable-rules","service":{"ports":[{"name":"tcp-48100","protocol":"TCP","port":48100,"targetPort":48100}],"selector":{"app":"edgex-app-service-configurable-rules"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-app-service-configurable-rules"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-app-service-configurable-rules"}},"spec":{"containers":[{"name":"edgex-app-service-configurable-rules","image":"openyurt/docker-app-service-configurable:1.3.1","ports":[{"name":"tcp-48100","containerPort":48100,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"BINDING_PUBLISHTOPIC","value":"events"},{"name":"SERVICE_HOST","value":"edgex-app-service-configurable-rules"},{"name":"MESSAGEBUS_SUBSCRIBEHOST_HOST","value":"edgex-core-data"},{"name":"SERVICE_PORT","value":"48100"},{"name":"EDGEX_PROFILE","value":"rules-engine"}],"resources":{},"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-app-service-configurable-rules"}},"strategy":{}}},{"name":"edgex-core-metadata","service":{"ports":[{"name":"tcp-48081","protocol":"TCP","port":48081,"targetPort":48081}],"selector":{"app":"edgex-core-metadata"}},"deployment":{"selector":{"matchLabels":{"app":"edgex-core-metadata"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"edgex-core-metadata"}},"spec":{"volumes":[{"name":"anonymous-volume1","hostPath":{"path":"/tmp/edgex/secrets/ca","type":"DirectoryOrCreate"}},{"name":"anonymous-volume2","hostPath":{"path":"/tmp/edgex/secrets/edgex-core-metadata","type":"DirectoryOrCreate"}}],"containers":[{"name":"edgex-core-metadata","image":"openyurt/docker-core-metadata-go:1.3.1","ports":[{"name":"tcp-48081","containerPort":48081,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"common-variable-hanoi"}}],"env":[{"name":"SERVICE_HOST","value":"edgex-core-metadata"},{"name":"SECRETSTORE_TOKENFILE","value":"/tmp/edgex/secrets/edgex-core-metadata/secrets-token.json"},{"name":"NOTIFICATIONS_SENDER","value":"edgex-core-metadata"}],"resources":{},"volumeMounts":[{"name":"anonymous-volume1","mountPath":"/tmp/edgex/secrets/ca"},{"name":"anonymous-volume2","mountPath":"/tmp/edgex/secrets/edgex-core-metadata"}],"imagePullPolicy":"IfNotPresent"}],"hostname":"edgex-core-metadata"}},"strategy":{}}}]}]} \ No newline at end of file +{ + "versions": [ + { + "versionName": "kamakura", + "configMaps": [ + { + "metadata": { + "name": "common-variable-kamakura", + "creationTimestamp": null + }, + "data": { + "API_GATEWAY_HOST": "edgex-kong", + "API_GATEWAY_STATUS_PORT": "8100", + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "true", + "MESSAGEQUEUE_HOST": "edgex-redis", + "PROXY_SETUP_HOST": "edgex-security-proxy-setup", + "REGISTRY_HOST": "edgex-core-consul", + "SECRETSTORE_HOST": "edgex-vault", + "SECRETSTORE_PORT": "8200", + "SPIFFE_ENDPOINTSOCKET": "/tmp/edgex/secrets/spiffe/public/api.sock", + "SPIFFE_TRUSTBUNDLE_PATH": "/tmp/edgex/secrets/spiffe/trust/bundle", + "SPIFFE_TRUSTDOMAIN": "edgexfoundry.org", + "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", + "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", + "STAGEGATE_DATABASE_HOST": "edgex-redis", + "STAGEGATE_DATABASE_PORT": "6379", + "STAGEGATE_DATABASE_READYPORT": "6379", + "STAGEGATE_KONGDB_HOST": "edgex-kong-db", + "STAGEGATE_KONGDB_PORT": "5432", + "STAGEGATE_KONGDB_READYPORT": "54325", + "STAGEGATE_READY_TORUNPORT": "54329", + "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", + "STAGEGATE_REGISTRY_PORT": "8500", + "STAGEGATE_REGISTRY_READYPORT": "54324", + "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", + "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", + "STAGEGATE_WAITFOR_TIMEOUT": "60s" + } + } + ], + "components": [ + { + "name": "edgex-security-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrapper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:2.2.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-bootstrapper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "kuiper-data", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.4.4-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + }, + { + "name": "kuiper-connections", + "mountPath": "/kuiper/etc/connections" + }, + { + "name": "kuiper-sources", + "mountPath": "/kuiper/etc/sources" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-virtual", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:2.2.0", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-virtual" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/sys-mgmt-agent:2.2.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kong", + "service": { + "ports": [ + { + "name": "tcp-8000", + "protocol": "TCP", + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8100", + "protocol": "TCP", + "port": 8100, + "targetPort": 8100 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 + } + ], + "selector": { + "app": "edgex-kong" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong", + "image": "kong:2.6.1", + "ports": [ + { + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8100", + "containerPort": 8100, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_PG_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "KONG_DNS_ORDER", + "value": "LAST,A,CNAME" + }, + { + "name": "KONG_PG_HOST", + "value": "edgex-kong-db" + }, + { + "name": "KONG_SSL_CIPHER_SUITE", + "value": "modern" + }, + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_STATUS_LISTEN", + "value": "0.0.0.0:8100" + }, + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" + }, + { + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_NGINX_WORKER_PROCESSES", + "value": "1" + }, + { + "name": "KONG_DNS_VALID_TTL", + "value": "1" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kong-db", + "service": { + "ports": [ + { + "name": "tcp-5432", + "protocol": "TCP", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "app": "edgex-kong-db" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong-db" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong-db" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "postgres-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong-db", + "image": "postgres:13.5-alpine", + "ports": [ + { + "name": "tcp-5432", + "containerPort": 5432, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "POSTGRES_USER", + "value": "kong" + }, + { + "name": "POSTGRES_DB", + "value": "kong" + }, + { + "name": "POSTGRES_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/var/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/run" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong-db" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-vault", + "service": { + "ports": [ + { + "name": "tcp-8200", + "protocol": "TCP", + "port": 8200, + "targetPort": 8200 + } + ], + "selector": { + "app": "edgex-vault" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-vault" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-vault" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-vault", + "image": "vault:1.8.9", + "ports": [ + { + "name": "tcp-8200", + "containerPort": 8200, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "VAULT_UI", + "value": "true" + }, + { + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" + }, + { + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-vault" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "db-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "redis-config", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "redis:6.2.6-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" + }, + { + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "db-data", + "mountPath": "/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "redis-config", + "mountPath": "/run/redis/conf" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-data", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:2.2.0", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/core-data/secrets-token.json" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-rest", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:2.2.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-rest" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-notifications", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:2.2.0", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-notifications" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-proxy-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-proxy-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-proxy-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:2.2.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ADD_PROXY_ROUTE" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "KONGURL_SERVER", + "value": "edgex-kong" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + }, + { + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-proxy-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-ui-go", + "service": { + "ports": [ + { + "name": "tcp-4000", + "protocol": "TCP", + "port": 4000, + "targetPort": 4000 + } + ], + "selector": { + "app": "edgex-ui-go" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-ui-go" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-ui-go" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-ui-go", + "image": "edgexfoundry/edgex-ui:2.2.0", + "ports": [ + { + "name": "tcp-4000", + "containerPort": 4000, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-ui-go" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-consul", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "consul:1.10.10", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "ADD_REGISTRY_ACL_ROLES" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/edgex-consul" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-command", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:2.2.0", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-command" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/app-rules-engine", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:2.2.0", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/app-rules-engine" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-scheduler", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:2.2.0", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-scheduler" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-metadata", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:2.2.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-metadata" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-secretstore-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-secretstore-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-secretstore-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets", + "type": "DirectoryOrCreate" + } + }, + { + "name": "kong", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "vault-config", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:2.2.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-kamakura" + } + } + ], + "env": [ + { + "name": "ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" + }, + { + "name": "ADD_SECRETSTORE_TOKENS" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" + }, + { + "name": "kong", + "mountPath": "/tmp/kong" + }, + { + "name": "kuiper-sources", + "mountPath": "/tmp/kuiper" + }, + { + "name": "kuiper-connections", + "mountPath": "/tmp/kuiper-connections" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-secretstore-setup" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "jakarta", + "configMaps": [ + { + "metadata": { + "name": "common-variable-jakarta", + "creationTimestamp": null + }, + "data": { + "API_GATEWAY_HOST": "edgex-kong", + "API_GATEWAY_STATUS_PORT": "8100", + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "true", + "MESSAGEQUEUE_HOST": "edgex-redis", + "PROXY_SETUP_HOST": "edgex-security-proxy-setup", + "REGISTRY_HOST": "edgex-core-consul", + "SECRETSTORE_HOST": "edgex-vault", + "SECRETSTORE_PORT": "8200", + "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", + "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", + "STAGEGATE_DATABASE_HOST": "edgex-redis", + "STAGEGATE_DATABASE_PORT": "6379", + "STAGEGATE_DATABASE_READYPORT": "6379", + "STAGEGATE_KONGDB_HOST": "edgex-kong-db", + "STAGEGATE_KONGDB_PORT": "5432", + "STAGEGATE_KONGDB_READYPORT": "54325", + "STAGEGATE_READY_TORUNPORT": "54329", + "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", + "STAGEGATE_REGISTRY_PORT": "8500", + "STAGEGATE_REGISTRY_READYPORT": "54324", + "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", + "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", + "STAGEGATE_WAITFOR_TIMEOUT": "60s" + } + } + ], + "components": [ + { + "name": "edgex-vault", + "service": { + "ports": [ + { + "name": "tcp-8200", + "protocol": "TCP", + "port": 8200, + "targetPort": 8200 + } + ], + "selector": { + "app": "edgex-vault" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-vault" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-vault" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-vault", + "image": "vault:1.8.4", + "ports": [ + { + "name": "tcp-8200", + "containerPort": 8200, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "VAULT_UI", + "value": "true" + }, + { + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" + }, + { + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-vault" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-command", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:2.1.1", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-command" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-consul", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "consul:1.10.3", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "ADD_REGISTRY_ACL_ROLES" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/edgex-consul" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-secretstore-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-secretstore-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-secretstore-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets", + "type": "DirectoryOrCreate" + } + }, + { + "name": "kong", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "vault-config", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:2.1.1", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "ADD_SECRETSTORE_TOKENS" + }, + { + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" + }, + { + "name": "kong", + "mountPath": "/tmp/kong" + }, + { + "name": "kuiper-sources", + "mountPath": "/tmp/kuiper" + }, + { + "name": "kuiper-connections", + "mountPath": "/tmp/kuiper-connections" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-secretstore-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "db-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "redis-config", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "redis:6.2.6-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" + }, + { + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "db-data", + "mountPath": "/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "redis-config", + "mountPath": "/run/redis/conf" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-proxy-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-proxy-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-proxy-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:2.1.1", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + }, + { + "name": "KONGURL_SERVER", + "value": "edgex-kong" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + }, + { + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, + { + "name": "ADD_PROXY_ROUTE" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-proxy-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "kuiper-data", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.4.4-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + }, + { + "name": "kuiper-connections", + "mountPath": "/kuiper/etc/connections" + }, + { + "name": "kuiper-sources", + "mountPath": "/kuiper/etc/sources" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kong-db", + "service": { + "ports": [ + { + "name": "tcp-5432", + "protocol": "TCP", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "app": "edgex-kong-db" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong-db" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong-db" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "postgres-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong-db", + "image": "postgres:13.4-alpine", + "ports": [ + { + "name": "tcp-5432", + "containerPort": 5432, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "POSTGRES_DB", + "value": "kong" + }, + { + "name": "POSTGRES_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "POSTGRES_USER", + "value": "kong" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/var/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/run" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong-db" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-metadata", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:2.1.1", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-metadata" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kong", + "service": { + "ports": [ + { + "name": "tcp-8000", + "protocol": "TCP", + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8100", + "protocol": "TCP", + "port": 8100, + "targetPort": 8100 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 + } + ], + "selector": { + "app": "edgex-kong" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong", + "image": "kong:2.5.1", + "ports": [ + { + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8100", + "containerPort": 8100, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_DNS_VALID_TTL", + "value": "1" + }, + { + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_SSL_CIPHER_SUITE", + "value": "modern" + }, + { + "name": "KONG_PG_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" + }, + { + "name": "KONG_STATUS_LISTEN", + "value": "0.0.0.0:8100" + }, + { + "name": "KONG_NGINX_WORKER_PROCESSES", + "value": "1" + }, + { + "name": "KONG_DNS_ORDER", + "value": "LAST,A,CNAME" + }, + { + "name": "KONG_PG_HOST", + "value": "edgex-kong-db" + }, + { + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-virtual", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:2.1.1", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-virtual" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-ui-go", + "service": { + "ports": [ + { + "name": "tcp-4000", + "protocol": "TCP", + "port": 4000, + "targetPort": 4000 + } + ], + "selector": { + "app": "edgex-ui-go" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-ui-go" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-ui-go" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-ui-go", + "image": "edgexfoundry/edgex-ui:2.1.0", + "ports": [ + { + "name": "tcp-4000", + "containerPort": 4000, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-ui-go" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/app-rules-engine", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:2.1.2", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/app-rules-engine" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-notifications", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:2.1.1", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-notifications" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-rest", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:2.1.1", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-rest" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-data", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:2.1.1", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + }, + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/core-data/secrets-token.json" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/sys-mgmt-agent:2.1.1", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrapper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:2.1.1", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-bootstrapper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-scheduler", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:2.1.1", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-jakarta" + } + } + ], + "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-scheduler" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "levski", + "configMaps": [ + { + "metadata": { + "name": "common-variable-levski", + "creationTimestamp": null + }, + "data": { + "API_GATEWAY_HOST": "edgex-kong", + "API_GATEWAY_STATUS_PORT": "8100", + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "true", + "MESSAGEQUEUE_HOST": "edgex-redis", + "PROXY_SETUP_HOST": "edgex-security-proxy-setup", + "REGISTRY_HOST": "edgex-core-consul", + "SECRETSTORE_HOST": "edgex-vault", + "SECRETSTORE_PORT": "8200", + "SPIFFE_ENDPOINTSOCKET": "/tmp/edgex/secrets/spiffe/public/api.sock", + "SPIFFE_TRUSTBUNDLE_PATH": "/tmp/edgex/secrets/spiffe/trust/bundle", + "SPIFFE_TRUSTDOMAIN": "edgexfoundry.org", + "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", + "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", + "STAGEGATE_DATABASE_HOST": "edgex-redis", + "STAGEGATE_DATABASE_PORT": "6379", + "STAGEGATE_DATABASE_READYPORT": "6379", + "STAGEGATE_KONGDB_HOST": "edgex-kong-db", + "STAGEGATE_KONGDB_PORT": "5432", + "STAGEGATE_KONGDB_READYPORT": "54325", + "STAGEGATE_READY_TORUNPORT": "54329", + "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", + "STAGEGATE_REGISTRY_PORT": "8500", + "STAGEGATE_REGISTRY_READYPORT": "54324", + "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", + "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", + "STAGEGATE_WAITFOR_TIMEOUT": "60s" + } + } + ], + "components": [ + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/sys-mgmt-agent:2.3.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-ui-go", + "service": { + "ports": [ + { + "name": "tcp-4000", + "protocol": "TCP", + "port": 4000, + "targetPort": 4000 + } + ], + "selector": { + "app": "edgex-ui-go" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-ui-go" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-ui-go" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-ui-go", + "image": "edgexfoundry/edgex-ui:2.3.0", + "ports": [ + { + "name": "tcp-4000", + "containerPort": 4000, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-ui-go" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-ui-go" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-rest", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:2.3.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-rest" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-data", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:2.3.0", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + }, + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/core-data/secrets-token.json" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-notifications", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:2.3.0", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-notifications" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-command", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:2.3.0", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "MESSAGEQUEUE_EXTERNAL_URL", + "value": "tcp://edgex-mqtt-broker:1883" + }, + { + "name": "MESSAGEQUEUE_INTERNAL_HOST", + "value": "edgex-redis" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-command" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-consul", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "consul:1.13.2", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_MANAGEMENTTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/mgmt_token.json" + }, + { + "name": "ADD_REGISTRY_ACL_ROLES" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/edgex-consul" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-virtual", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:2.3.0", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-virtual" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "db-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "redis-config", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "redis:7.0.5-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" + }, + { + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "db-data", + "mountPath": "/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "redis-config", + "mountPath": "/run/redis/conf" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/app-rules-engine", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:2.3.1", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/app-rules-engine" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "kuiper-data", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.7.1-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + }, + { + "name": "kuiper-connections", + "mountPath": "/kuiper/etc/connections" + }, + { + "name": "kuiper-sources", + "mountPath": "/kuiper/etc/sources" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-scheduler", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:2.3.0", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-scheduler" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrapper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:2.3.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-bootstrapper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-proxy-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-proxy-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-proxy-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:2.3.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, + { + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ADD_PROXY_ROUTE" + }, + { + "name": "KONGURL_SERVER", + "value": "edgex-kong" + }, + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + }, + { + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-proxy-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-vault", + "service": { + "ports": [ + { + "name": "tcp-8200", + "protocol": "TCP", + "port": 8200, + "targetPort": 8200 + } + ], + "selector": { + "app": "edgex-vault" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-vault" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-vault" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-vault", + "image": "vault:1.11.4", + "ports": [ + { + "name": "tcp-8200", + "containerPort": 8200, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" + }, + { + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" + }, + { + "name": "VAULT_UI", + "value": "true" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-vault" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kong", + "service": { + "ports": [ + { + "name": "tcp-8000", + "protocol": "TCP", + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8100", + "protocol": "TCP", + "port": 8100, + "targetPort": 8100 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 + } + ], + "selector": { + "app": "edgex-kong" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong", + "image": "kong:2.8.1", + "ports": [ + { + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8100", + "containerPort": 8100, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_DNS_ORDER", + "value": "LAST,A,CNAME" + }, + { + "name": "KONG_DNS_VALID_TTL", + "value": "1" + }, + { + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_NGINX_WORKER_PROCESSES", + "value": "1" + }, + { + "name": "KONG_PG_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "KONG_PG_HOST", + "value": "edgex-kong-db" + }, + { + "name": "KONG_SSL_CIPHER_SUITE", + "value": "modern" + }, + { + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_STATUS_LISTEN", + "value": "0.0.0.0:8100" + }, + { + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kong-db", + "service": { + "ports": [ + { + "name": "tcp-5432", + "protocol": "TCP", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "app": "edgex-kong-db" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong-db" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong-db" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "postgres-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong-db", + "image": "postgres:13.8-alpine", + "ports": [ + { + "name": "tcp-5432", + "containerPort": 5432, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "POSTGRES_USER", + "value": "kong" + }, + { + "name": "POSTGRES_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "POSTGRES_DB", + "value": "kong" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/var/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/run" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong-db" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-secretstore-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-secretstore-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-secretstore-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets", + "type": "DirectoryOrCreate" + } + }, + { + "name": "kong", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "vault-config", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:2.3.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],message-bus[device-rest],redisdb[device-virtual],message-bus[device-virtual]" + }, + { + "name": "ADD_SECRETSTORE_TOKENS" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" + }, + { + "name": "kong", + "mountPath": "/tmp/kong" + }, + { + "name": "kuiper-sources", + "mountPath": "/tmp/kuiper" + }, + { + "name": "kuiper-connections", + "mountPath": "/tmp/kuiper-connections" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-secretstore-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-metadata", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:2.3.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-levski" + } + } + ], + "env": [ + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-metadata" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "minnesota", + "configMaps": [ + { + "metadata": { + "name": "common-variable-minnesota", + "creationTimestamp": null + }, + "data": { + "EDGEX_SECURITY_SECRET_STORE": "true", + "PROXY_SETUP_HOST": "edgex-security-proxy-setup", + "SECRETSTORE_HOST": "edgex-vault", + "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", + "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", + "STAGEGATE_DATABASE_HOST": "edgex-redis", + "STAGEGATE_DATABASE_PORT": "6379", + "STAGEGATE_DATABASE_READYPORT": "6379", + "STAGEGATE_READY_TORUNPORT": "54329", + "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", + "STAGEGATE_REGISTRY_PORT": "8500", + "STAGEGATE_REGISTRY_READYPORT": "54324", + "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", + "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", + "STAGEGATE_WAITFOR_TIMEOUT": "60s" + } + } + ], + "components": [ + { + "name": "edgex-security-secretstore-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-secretstore-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-secretstore-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets", + "type": "DirectoryOrCreate" + } + }, + { + "name": "kuiper-sources", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "vault-config", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:3.0.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" + }, + { + "name": "EDGEX_ADD_SECRETSTORE_TOKENS" + }, + { + "name": "EDGEX_ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],message-bus[device-rest],redisdb[device-virtual],message-bus[device-virtual]" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" + }, + { + "name": "kuiper-sources", + "mountPath": "/tmp/kuiper" + }, + { + "name": "kuiper-connections", + "mountPath": "/tmp/kuiper-connections" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-secretstore-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-data", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:3.0.0", + "ports": [ + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-scheduler", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:3.0.0", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-scheduler" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-proxy-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-proxy-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-proxy-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "vault-config", + "emptyDir": {} + }, + { + "name": "nginx-templates", + "emptyDir": {} + }, + { + "name": "nginx-tls", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + }, + { + "name": "consul-acl-token", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:3.0.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" + }, + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "EDGEX_ADD_PROXY_ROUTE" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" + }, + { + "name": "nginx-templates", + "mountPath": "/etc/nginx/templates" + }, + { + "name": "nginx-tls", + "mountPath": "/etc/ssl/nginx" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-proxy-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-rest", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:3.0.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-rest" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "kuiper-data", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} + }, + { + "name": "kuiper-log", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.9.2-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "edgex/rules-events" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + }, + { + "name": "kuiper-connections", + "mountPath": "/kuiper/etc/connections" + }, + { + "name": "kuiper-sources", + "mountPath": "/kuiper/etc/sources" + }, + { + "name": "kuiper-log", + "mountPath": "/kuiper/log" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-ui-go", + "service": { + "ports": [ + { + "name": "tcp-4000", + "protocol": "TCP", + "port": 4000, + "targetPort": 4000 + } + ], + "selector": { + "app": "edgex-ui-go" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-ui-go" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-ui-go" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-ui-go", + "image": "edgexfoundry/edgex-ui:3.0.0", + "ports": [ + { + "name": "tcp-4000", + "containerPort": 4000, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-ui-go" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-ui-go" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-vault", + "service": { + "ports": [ + { + "name": "tcp-8200", + "protocol": "TCP", + "port": 8200, + "targetPort": 8200 + } + ], + "selector": { + "app": "edgex-vault" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-vault" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-vault" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-vault", + "image": "hashicorp/vault:1.13.2", + "ports": [ + { + "name": "tcp-8200", + "containerPort": 8200, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "VAULT_UI", + "value": "true" + }, + { + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" + }, + { + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-vault" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-consul", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "hashicorp/consul:1.15.2", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_MANAGEMENTTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/mgmt_token.json" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_ADD_REGISTRY_ACL_ROLES" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/edgex-consul" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-metadata", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:3.0.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-metadata" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrapper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:3.0.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "EDGEX_USER", + "value": "2002" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-bootstrapper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-proxy-auth", + "service": { + "ports": [ + { + "name": "tcp-59842", + "protocol": "TCP", + "port": 59842, + "targetPort": 59842 + } + ], + "selector": { + "app": "edgex-proxy-auth" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-proxy-auth" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-proxy-auth" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-auth", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-proxy-auth", + "image": "edgexfoundry/security-proxy-auth:3.0.0", + "ports": [ + { + "name": "tcp-59842", + "containerPort": 59842, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-proxy-auth" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-auth" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-proxy-auth" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-nginx", + "service": { + "ports": [ + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 + } + ], + "selector": { + "app": "edgex-nginx" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-nginx" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-nginx" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, + { + "name": "tmpfs-volume4", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "nginx-templates", + "emptyDir": {} + }, + { + "name": "nginx-tls", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-nginx", + "image": "nginx:1.24.0-alpine-slim", + "ports": [ + { + "name": "tcp-8443", + "containerPort": 8443, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/etc/nginx/conf.d" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/var/cache/nginx" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/var/log/nginx" + }, + { + "name": "tmpfs-volume4", + "mountPath": "/var/run" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "nginx-templates", + "mountPath": "/etc/nginx/templates" + }, + { + "name": "nginx-tls", + "mountPath": "/etc/ssl/nginx" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-nginx" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-notifications", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:3.0.0", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-notifications" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/app-rules-engine", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:3.0.0", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/app-rules-engine" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-common-config-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-common-config-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-common-config-bootstrapper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-common-config-bootstrapper", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-common-config-bootstrapper", + "image": "edgexfoundry/core-common-config-bootstrapper:3.0.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "APP_SERVICES_CLIENTS_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ALL_SERVICES_MESSAGEBUS_HOST", + "value": "edgex-redis" + }, + { + "name": "ALL_SERVICES_DATABASE_HOST", + "value": "edgex-redis" + }, + { + "name": "DEVICE_SERVICES_CLIENTS_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ALL_SERVICES_REGISTRY_HOST", + "value": "edgex-core-consul" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-common-config-bootstrapper" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-common-config-bootstrapper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-command", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:3.0.0", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "EXTERNALMQTT_URL", + "value": "tcp://edgex-mqtt-broker:1883" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-command" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "db-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "redis-config", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "redis:7.0.11-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" + }, + { + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "db-data", + "mountPath": "/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "redis-config", + "mountPath": "/run/redis/conf" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-virtual", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:3.0.0", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-minnesota" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-virtual" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "ireland", + "configMaps": [ + { + "metadata": { + "name": "common-variable-ireland", + "creationTimestamp": null + }, + "data": { + "API_GATEWAY_HOST": "edgex-kong", + "API_GATEWAY_STATUS_PORT": "8100", + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "true", + "MESSAGEQUEUE_HOST": "edgex-redis", + "PROXY_SETUP_HOST": "edgex-security-proxy-setup", + "REGISTRY_HOST": "edgex-core-consul", + "SECRETSTORE_HOST": "edgex-vault", + "SECRETSTORE_PORT": "8200", + "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", + "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", + "STAGEGATE_DATABASE_HOST": "edgex-redis", + "STAGEGATE_DATABASE_PORT": "6379", + "STAGEGATE_DATABASE_READYPORT": "6379", + "STAGEGATE_KONGDB_HOST": "edgex-kong-db", + "STAGEGATE_KONGDB_PORT": "5432", + "STAGEGATE_KONGDB_READYPORT": "54325", + "STAGEGATE_READY_TORUNPORT": "54329", + "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", + "STAGEGATE_REGISTRY_PORT": "8500", + "STAGEGATE_REGISTRY_READYPORT": "54324", + "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", + "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", + "STAGEGATE_WAITFOR_TIMEOUT": "60s" + } + } + ], + "components": [ + { + "name": "edgex-security-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrapper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:2.0.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-bootstrapper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-vault", + "service": { + "ports": [ + { + "name": "tcp-8200", + "protocol": "TCP", + "port": 8200, + "targetPort": 8200 + } + ], + "selector": { + "app": "edgex-vault" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-vault" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-vault" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-vault", + "image": "vault:1.7.2", + "ports": [ + { + "name": "tcp-8200", + "containerPort": 8200, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" + }, + { + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" + }, + { + "name": "VAULT_UI", + "value": "true" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-vault" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-command", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:2.0.0", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-command" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-rules-engine" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-rules-engine" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/app-rules-engine", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:2.0.1", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/app-rules-engine" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-rules-engine" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-rest", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:2.0.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-rest" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-59900", + "protocol": "TCP", + "port": 59900, + "targetPort": 59900 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/device-virtual", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:2.0.0", + "ports": [ + { + "name": "tcp-59900", + "containerPort": 59900, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-virtual" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "kuiper-data", + "emptyDir": {} + }, + { + "name": "kuiper-config", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.3.0-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + }, + { + "name": "kuiper-config", + "mountPath": "/kuiper/etc/sources" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-secretstore-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-secretstore-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-secretstore-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets", + "type": "DirectoryOrCreate" + } + }, + { + "name": "kong", + "emptyDir": {} + }, + { + "name": "kuiper-config", + "emptyDir": {} + }, + { + "name": "vault-config", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:2.0.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "ADD_SECRETSTORE_TOKENS" + }, + { + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" + }, + { + "name": "kong", + "mountPath": "/tmp/kong" + }, + { + "name": "kuiper-config", + "mountPath": "/tmp/kuiper" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-secretstore-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kong-db", + "service": { + "ports": [ + { + "name": "tcp-5432", + "protocol": "TCP", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "app": "edgex-kong-db" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong-db" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong-db" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "postgres-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong-db", + "image": "postgres:12.3-alpine", + "ports": [ + { + "name": "tcp-5432", + "containerPort": 5432, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "POSTGRES_USER", + "value": "kong" + }, + { + "name": "POSTGRES_DB", + "value": "kong" + }, + { + "name": "POSTGRES_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/var/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/run" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong-db" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-consul", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "consul:1.9.5", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" + }, + { + "name": "ADD_REGISTRY_ACL_ROLES" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/edgex-consul" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-data", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:2.0.0", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + }, + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/core-data/secrets-token.json" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-59860", + "protocol": "TCP", + "port": 59860, + "targetPort": 59860 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-notifications", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:2.0.0", + "ports": [ + { + "name": "tcp-59860", + "containerPort": 59860, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-notifications" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-proxy-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-proxy-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-proxy-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:2.0.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + }, + { + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "ADD_PROXY_ROUTE" + }, + { + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + }, + { + "name": "KONGURL_SERVER", + "value": "edgex-kong" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-proxy-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "db-data", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "redis-config", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "redis:6.2.4-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" + }, + { + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "db-data", + "mountPath": "/data" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "redis-config", + "mountPath": "/run/redis/conf" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-kong", + "service": { + "ports": [ + { + "name": "tcp-8000", + "protocol": "TCP", + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8100", + "protocol": "TCP", + "port": 8100, + "targetPort": 8100 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 + } + ], + "selector": { + "app": "edgex-kong" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-proxy-setup", + "type": "DirectoryOrCreate" + } + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong", + "image": "kong:2.4.1-alpine", + "ports": [ + { + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8100", + "containerPort": 8100, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "KONG_DNS_ORDER", + "value": "LAST,A,CNAME" + }, + { + "name": "KONG_PG_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" + }, + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_STATUS_LISTEN", + "value": "0.0.0.0:8100" + }, + { + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_DNS_VALID_TTL", + "value": "1" + }, + { + "name": "KONG_PG_HOST", + "value": "edgex-kong-db" + }, + { + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-59861", + "protocol": "TCP", + "port": 59861, + "targetPort": 59861 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-scheduler", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:2.0.0", + "ports": [ + { + "name": "tcp-59861", + "containerPort": 59861, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-scheduler" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/sys-mgmt-agent:2.0.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-metadata", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:2.0.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-ireland" + } + } + ], + "env": [ + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-metadata" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + } + ] + }, + { + "versionName": "hanoi", + "configMaps": [ + { + "metadata": { + "name": "common-variable-hanoi", + "creationTimestamp": null + }, + "data": { + "CLIENTS_COMMAND_HOST": "edgex-core-command", + "CLIENTS_COREDATA_HOST": "edgex-core-data", + "CLIENTS_DATA_HOST": "edgex-core-data", + "CLIENTS_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_RULESENGINE_HOST": "edgex-kuiper", + "CLIENTS_SCHEDULER_HOST": "edgex-support-scheduler", + "CLIENTS_VIRTUALDEVICE_HOST": "edgex-device-virtual", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "true", + "LOGGING_ENABLEREMOTE": "false", + "REGISTRY_HOST": "edgex-core-consul", + "SECRETSTORE_HOST": "edgex-vault", + "SECRETSTORE_ROOTCACERTPATH": "/tmp/edgex/secrets/ca/ca.pem", + "SECRETSTORE_SERVERNAME": "edgex-vault", + "SERVICE_SERVERBINDADDR": "0.0.0.0" + } + } + ], + "components": [ + { + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-20498", + "protocol": "TCP", + "port": 20498, + "targetPort": 20498 + }, + { + "name": "tcp-48075", + "protocol": "TCP", + "port": 48075, + "targetPort": 48075 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kuiper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kuiper" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-kuiper", + "image": "emqx/kuiper:1.1.1-alpine", + "ports": [ + { + "name": "tcp-20498", + "containerPort": 20498, + "protocol": "TCP" + }, + { + "name": "tcp-48075", + "containerPort": 48075, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "EDGEX__DEFAULT__PORT", + "value": "5566" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "tcp" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-app-service-configurable-rules" + }, + { + "name": "EDGEX__DEFAULT__SERVICESERVER", + "value": "http://edgex-core-data:48080" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "events" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "48075" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kuiper" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-secrets-setup", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-secrets-setup" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-secrets-setup" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "secrets-setup-cache", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets", + "type": "DirectoryOrCreate" + } + }, + { + "name": "vault-init", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-secrets-setup", + "image": "edgexfoundry/docker-security-secrets-setup-go:1.3.1", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/run" + }, + { + "name": "secrets-setup-cache", + "mountPath": "/etc/edgex/pki" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" + }, + { + "name": "vault-init", + "mountPath": "/vault/init" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-secrets-setup" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-proxy", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-proxy" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-proxy" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-scripts", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-security-proxy-setup", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-proxy", + "image": "edgexfoundry/docker-security-proxy-setup-go:1.3.1", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SECRETSERVICE_SNIS", + "value": "edgex-kong" + }, + { + "name": "SECRETSERVICE_TOKENPATH", + "value": "/tmp/edgex/secrets/edgex-security-proxy-setup/secrets-token.json" + }, + { + "name": "KONGURL_SERVER", + "value": "kong" + }, + { + "name": "SECRETSERVICE_CACERTPATH", + "value": "/tmp/edgex/secrets/ca/ca.pem" + }, + { + "name": "SECRETSERVICE_SERVER", + "value": "edgex-vault" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-security-proxy-setup" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-proxy" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-bootstrap-database", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-bootstrap-database" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrap-database" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-security-bootstrap-redis", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-security-bootstrap-database", + "image": "edgexfoundry/docker-security-bootstrap-redis-go:1.3.1", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-security-bootstrap-redis/secrets-token.json" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-security-bootstrap-database" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-security-bootstrap-redis" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-bootstrap-database" + } + }, + "strategy": {} + } + }, + { + "name": "kong", + "service": { + "ports": [ + { + "name": "tcp-8000", + "protocol": "TCP", + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8001", + "protocol": "TCP", + "port": 8001, + "targetPort": 8001 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 + }, + { + "name": "tcp-8444", + "protocol": "TCP", + "port": 8444, + "targetPort": 8444 + } + ], + "selector": { + "app": "kong" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "kong" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "kong" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "kong", + "image": "kong:2.0.5", + "ports": [ + { + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8001", + "containerPort": 8001, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, + "protocol": "TCP" + }, + { + "name": "tcp-8444", + "containerPort": 8444, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_PG_HOST", + "value": "kong-db" + }, + { + "name": "KONG_PG_PASSWORD", + "value": "kong" + }, + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "0.0.0.0:8001, 0.0.0.0:8444 ssl" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "kong" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-app-service-configurable-rules", + "service": { + "ports": [ + { + "name": "tcp-48100", + "protocol": "TCP", + "port": 48100, + "targetPort": 48100 + } + ], + "selector": { + "app": "edgex-app-service-configurable-rules" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-app-service-configurable-rules" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-app-service-configurable-rules" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-app-service-configurable-rules", + "image": "edgexfoundry/docker-app-service-configurable:1.3.1", + "ports": [ + { + "name": "tcp-48100", + "containerPort": 48100, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "BINDING_PUBLISHTOPIC", + "value": "events" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-app-service-configurable-rules" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "SERVICE_PORT", + "value": "48100" + }, + { + "name": "MESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-app-service-configurable-rules" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-48090", + "protocol": "TCP", + "port": 48090, + "targetPort": 48090 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/docker-sys-mgmt-agent-go:1.3.1", + "ports": [ + { + "name": "tcp-48090", + "containerPort": 48090, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-vault", + "service": { + "ports": [ + { + "name": "tcp-8200", + "protocol": "TCP", + "port": 8200, + "targetPort": 8200 + } + ], + "selector": { + "app": "edgex-vault" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-vault" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-vault" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-vault", + "type": "DirectoryOrCreate" + } + }, + { + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-init", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-vault", + "image": "vault:1.5.3", + "ports": [ + { + "name": "tcp-8200", + "containerPort": 8200, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "VAULT_UI", + "value": "true" + }, + { + "name": "VAULT_ADDR", + "value": "https://edgex-vault:8200" + }, + { + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/edgex-vault" + }, + { + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-init", + "mountPath": "/vault/init" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-vault" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-vault-worker", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-vault-worker" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-vault-worker" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets", + "type": "DirectoryOrCreate" + } + }, + { + "name": "vault-config", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-vault-worker", + "image": "edgexfoundry/docker-security-secretstore-setup-go:1.3.1", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SECRETSTORE_SETUP_DONE_FLAG", + "value": "/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-vault-worker" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-48081", + "protocol": "TCP", + "port": 48081, + "targetPort": 48081 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-metadata" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-metadata" + } + }, + "spec": { + "volumes": [ + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-core-metadata", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-metadata", + "image": "edgexfoundry/docker-core-metadata-go:1.3.1", + "ports": [ + { + "name": "tcp-48081", + "containerPort": 48081, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-core-metadata/secrets-token.json" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-core-metadata" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-metadata" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-consul" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-consul" + } + }, + "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-consul", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume3", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-kong", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume4", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-vault", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-consul", + "image": "edgexfoundry/docker-edgex-consul:1.3.0", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "EDGEX_SECURE", + "value": "true" + }, + { + "name": "SECRETSTORE_SETUP_DONE_FLAG", + "value": "/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done" + }, + { + "name": "EDGEX_DB", + "value": "redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-consul" + }, + { + "name": "anonymous-volume3", + "mountPath": "/tmp/edgex/secrets/edgex-kong" + }, + { + "name": "anonymous-volume4", + "mountPath": "/tmp/edgex/secrets/edgex-vault" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ + { + "name": "tcp-48085", + "protocol": "TCP", + "port": 48085, + "targetPort": 48085 + } + ], + "selector": { + "app": "edgex-support-scheduler" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-scheduler" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-scheduler" + } + }, + "spec": { + "volumes": [ + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-support-scheduler", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-scheduler", + "image": "edgexfoundry/docker-support-scheduler-go:1.3.1", + "ports": [ + { + "name": "tcp-48085", + "containerPort": 48085, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-support-scheduler/secrets-token.json" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-support-scheduler" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-scheduler" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-49986", + "protocol": "TCP", + "port": 49986, + "targetPort": 49986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-rest" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "edgexfoundry/docker-device-rest-go:1.2.1", + "ports": [ + { + "name": "tcp-49986", + "containerPort": 49986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-48082", + "protocol": "TCP", + "port": 48082, + "targetPort": 48082 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-command" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-command" + } + }, + "spec": { + "volumes": [ + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-core-command", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-command", + "image": "edgexfoundry/docker-core-command-go:1.3.1", + "ports": [ + { + "name": "tcp-48082", + "containerPort": 48082, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-core-command/secrets-token.json" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-core-command" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-command" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-redis" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-redis" + } + }, + "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-redis", + "image": "redis:6.0.9-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "db-data", + "mountPath": "/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-redis" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-48080", + "protocol": "TCP", + "port": 48080, + "targetPort": 48080 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "volumes": [ + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-core-data", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-data", + "image": "edgexfoundry/docker-core-data-go:1.3.1", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-48080", + "containerPort": 48080, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-data" + }, + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-core-data/secrets-token.json" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-core-data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-data" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-device-virtual", + "service": { + "ports": [ + { + "name": "tcp-49990", + "protocol": "TCP", + "port": 49990, + "targetPort": 49990 + } + ], + "selector": { + "app": "edgex-device-virtual" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-device-virtual" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-device-virtual" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-virtual", + "image": "edgexfoundry/docker-device-virtual-go:1.3.1", + "ports": [ + { + "name": "tcp-49990", + "containerPort": 49990, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-virtual" + } + }, + "strategy": {} + } + }, + { + "name": "kong-db", + "service": { + "ports": [ + { + "name": "tcp-5432", + "protocol": "TCP", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "app": "kong-db" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "kong-db" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "kong-db" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, + { + "name": "postgres-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "kong-db", + "image": "postgres:12.3-alpine", + "ports": [ + { + "name": "tcp-5432", + "containerPort": 5432, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "POSTGRES_USER", + "value": "kong" + }, + { + "name": "POSTGRES_DB", + "value": "kong" + }, + { + "name": "POSTGRES_PASSWORD", + "value": "kong" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/var/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/run" + }, + { + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "kong-db" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-notifications", + "service": { + "ports": [ + { + "name": "tcp-48060", + "protocol": "TCP", + "port": 48060, + "targetPort": 48060 + } + ], + "selector": { + "app": "edgex-support-notifications" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-support-notifications" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-support-notifications" + } + }, + "spec": { + "volumes": [ + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-support-notifications", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-support-notifications", + "image": "edgexfoundry/docker-support-notifications-go:1.3.1", + "ports": [ + { + "name": "tcp-48060", + "containerPort": 48060, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-support-notifications/secrets-token.json" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-support-notifications" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-support-notifications" + } + }, + "strategy": {} + } + }, + { + "name": "", + "deployment": { + "selector": { + "matchLabels": { + "app": "" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "", + "image": "kong:2.0.5", + "envFrom": [ + { + "configMapRef": { + "name": "common-variable-hanoi" + } + } + ], + "env": [ + { + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_PG_HOST", + "value": "kong-db" + }, + { + "name": "KONG_PG_PASSWORD", + "value": "kong" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/tmp" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ] + } + }, + "strategy": {} + } + } + ] + } + ] +} \ No newline at end of file diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml b/pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml index e079f05652f..a5f1096515f 100644 --- a/pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml +++ b/pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml @@ -1,9 +1,10 @@ -updated: "false" -count: 5 -latestVersion: levski +updated: false +count: 6 +latestVersion: minnesota versions: -- levski -- jakarta - kamakura +- jakarta +- levski +- minnesota - ireland - hanoi From b9022ba38d43b49dcaa9791bef2296a9cf33053a Mon Sep 17 00:00:00 2001 From: wesleysu <59680532+River-sh@users.noreply.github.com> Date: Fri, 7 Jul 2023 10:58:16 +0800 Subject: [PATCH 49/93] add proposal for raven l7 (#1541) * add proposal for raven l7 * add proposal for raven * f --------- Co-authored-by: Wesley Su --- docs/img/networking/img-4.png | Bin 0 -> 74008 bytes docs/img/networking/img-5.png | Bin 0 -> 90993 bytes docs/img/networking/img-6.png | Bin 0 -> 75556 bytes docs/img/networking/img-7.png | Bin 0 -> 72047 bytes docs/proposals/20230613-raven-l7-proxy.md | 235 ++++++++++++++++++ .../apps/v1alpha1/zz_generated.deepcopy.go | 2 +- .../apps/v1beta1/zz_generated.deepcopy.go | 2 +- .../raven/v1alpha1/zz_generated.deepcopy.go | 2 +- 8 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 docs/img/networking/img-4.png create mode 100644 docs/img/networking/img-5.png create mode 100644 docs/img/networking/img-6.png create mode 100644 docs/img/networking/img-7.png create mode 100644 docs/proposals/20230613-raven-l7-proxy.md diff --git a/docs/img/networking/img-4.png b/docs/img/networking/img-4.png new file mode 100644 index 0000000000000000000000000000000000000000..0579f0fd54f13e3e9d93e1fbc6acc2578249e0b1 GIT binary patch literal 74008 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 z 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 Nn$8XF@~d~A{C_tHdG7!K literal 0 HcmV?d00001 diff --git a/docs/img/networking/img-5.png b/docs/img/networking/img-5.png new file mode 100644 index 0000000000000000000000000000000000000000..0df154f0cbc8670a29fc750b350237c93e01232d GIT binary patch literal 90993 zcmeFZc|6qZ7e5@`TBx+4C{jr!RFdq9qHIOB8B1l4Y-4O=43(sXkbRAiWlYv#Fp4tv zH8a^p*1;Hq!7yWZK126?_x=99&-48MydHmjFrRC#>s;rY>zw!doGbLMp5|U|LGEqa zw(Y&GrD3pb8&}%4ZQIZ9+5!B<6Fi}_ZCl{B+ZxyJ`&iBl?)EoYO5Q-&G2&DdD2Rz6 zN0#?0bf(>lF?m%dyYDgbNoh~t_b`hAoH5cG9P5QKRU>n9rU#zQa~8a(wtxR$8BedP z?Rq`4zf6r&bcX36y1!_kT{V^3C@)X)clC%$gt*RD`znis)73q&Ly+0q9=Ao{v{t(3 z0bnkhKV50sD}O(4yO$Qoo+SI{BNx||t*31#B+sUC{C;-4&awCRqZSXh!|z8m&|S6d zzn{CFati)_e8+#dkQ_}w z@)G{qwoNU8Z6$#}92{kaEhAJcz^UmUP8O<5YLcF}JDoEx9(QZShd7lbwRyBcvg=eS zfT%k5u`TF?EXL!rzDcO+a;C8+H#2mm_os*8#O6yfU!mn^2xAGzFsQEN2f#}AA7xwR zJ3%XsCwywp_ebV^V4L*I!)$BT+RJrC^t6aN+l2R@+xi^0!woH!7i^O~VX*aGhdZ9X z^M5_N6Ui&~BP5>__{YJ2|Dp7H)fYbpX|O0N+cBs;KAW~%)R&(hN;$ontq4HGbdpKpNpMIJBN}CfHm) z+|eLksBRpjzDu-1aBFJp0q&|r=>V&h7r(`!yQ?wvf4p>wEs^!-n-cN6vPTVA{OPC< zfw$U1@39vp{(Dgmw?(D}o{8VXb;U#P4W~k$L79e4w~}}!wQsaHvXz!N3t_^ReWYx0 z3t8LL0)Jg- zJ7n;UKLov)Ql&n7wBja~IXC@9K3s3WrIoPgo4HEZO(I5QUUr#PiLYk7%pdSFFm_fx zS4>nqy*-=yUh#!<(L@wJcxz9R@m2C_BfRj-rr0wtTSROpyc0MW(vEK49n;`KCcR?4y90* z+d4DnZBfO7=gN=3Q{qnfG%fNCj<-qaHR%O4xdk;v!cuNpOd0QT3(RBT75fNr?3sm0 z`@}o#ZNQ;hAp`4$u%~bcP-noN4({FlP9uwF@~HN{7p`BAEP^s+O}Y~}`(%APIUstu zT#LMZ$mop1ggHlk?#UnO?62XSn(prLbucOaB1+NjeUyV(l=)g)I&||SRhPSf(NhF@ z%xmvDsho@o3{&+Ec$7(?B08`Tl1r+;a6xU*=vT;*rZr_~bk;L=*V4CbO zCu1cCRD7eNKt=dk!N`31z_ka@bR<ZrsqO0nmvqy2Zf_HjygYr2CLBs%_qHeIPJOK{Q|2m3 zQC$gXBaOE`(d9sj&3{vUxu@dRh_b3{IwB@^<8%=;LfI;VSI&Qx_aI^#b`Y`RAIgil zGu9JwGs}|)YgryhG&*UpQr_?~RlABnO0JQvL)gt4dd2=ksVk@eRtk8Vvuu;LDGCgj z`(8BHywiNcCKRey9*Bp>FO|rbtQLcG8Tpf#lHStTTD| z7X1Yt#+^#OWpa-!+{qZ4Gu7@~9dfl3io72wNk6a;Fl}zYRvvMK5jhne7+LXqpbKf* zL$bC{%D?QbYI~Xb+8KeGN)nfL6cn1H{8}RVUiF!cod4=B>2#>-mXAL9KDWbBuP1Y( z=p9#Db&b^$;ial}LnhYKBJx%?#D-t;PFAr#)B^l~#_q!%3|R!@u~V3wi8i&VbeW{B zaP>czXjyW}bk2ncRhTQB9X+O!UBL;;7@up*#e6Aeu6{I7la^3Ea4p;4D%z~{c#1%q z*r0J-$T4A+qXSrVlZU)1rP^2`(s6b~7dO5buW-o10vYT(HG7_#B*bu?s~?n)RXyzA zFw#dU{E{>jF74^7m*!?g&DYKb{bykyZ>?HeS6&gAr4mB5q5-yocLEiwn@D!`D0(3-$y zgsP5T0I?5r(~|6nz<#v;51Zni%=0UV=)?F?p}fZw#p!R)FAd(V+Yndq$19*RIYG_C zKjR;8C|9|~9@Wka#4lXc(Vnc~cP^g5bnX;+EIJS=pBL^!i&FkIw@`z;8vJ=^`)E@Y z4YBqURNV7Y8+~&1VO`-T>N8Hk{oIqDw%^}BxGq1z57U!zEDKB#IR84e+|;J^f<0^( zyn5~xxko0MW+0g+R3LIBas6{c7~e^%ifeeLff_1d_I(G|no*HhA<5Sa5-6Gmu}t z?|(=j+iqr0)vUzpeQ|u1!_^5?LCiTfO95(BBkJm@_GXvMh)AkyqK5%u+ zxcnecVD~XnO6ReL$f@V#SU(B124k2kIu&M=N#_nf3+P4CBORO3kliT zu3Z|4A3Zd?R~TXML*w%79f_ejnS_30TK>F3q!ou+)oqmaSAQ5jHgw1NxHr#a2mk4! z8QX|RdA+RVU173l>Xk-+Zl*l*qkzD3#0U_poVdw8U*pfGEr~u0s1YjcJ(gN=p`6>k zZ{%4Jt(L2VAMc0M;1wM}lz^}_x$JAYzyYurNGV;bM7Gd-FuEIpSC;|N86$OCiq4>zjo*B8I8o&mkeCact1C7xwF z{~H|1Db8_+E{LaAeM{-qNsvrST@PpuwqKDIA0QnAguD;+Yo9<1pM+2tB5ieE!?7Tc zSg7<_uGqy@w0oUDiOk>efv@}Wx+N2uQYm>Y+qtlMXmNt;D)~Wo%p>8+BUphh z(MziBf&m5bsjk3L`tmRK;cCQn!tJ!fp9D za$gIxM@FbM=9GPIV%f1yzBbI#Ed}bS>APyDvhS*8i&x^c*MboH_}^1?i%4QxdB7}& zuKCYq`F{!RtASjN760`(ScUUH@{*ZgX5F^D;fv_<+j51(wnhW_ZN= z*J3i>$*I2j&|1Qj*Qw#6^&YXd$Mc><%^yHmruQLG4?OY=D{$fL()H({7iZ#xuAwS8 z6*`+NH!5DM_Xkw<4#^WYz`D57@&faIPd%U4yU~G`vdTXGEBen+Mpv#}BNBlaL(25n z&It^c=WT~*`nMr4>JTD#itJ)+NS;5S%9|npLLQ^iqJNcx)xxpzKK|=x?S;ZLI$4G4 z^cxCi{tXEmG&qo8>yqySrJB`{I<69znh&@g?R{c{mS<)1^6F68j;b;gg(I$O5Ml``f4;Prl_9?FJ{di8F@cDRTch;9X$YgihVBfIlEIi z)SlNvIM4*!vPzxFPKs`1xYa029_~Ob?TPsyy^is2juj~sZCr|BPi zKFi5S`Kw?3w4sHec&utkUy_Ja+F;23Gur357N1GS`0`HikiHZ`g2=6=URs!N37%ty zH6j)X7a4)-)q3SCEXN@z-l?J&Q`wZ5&8xXMRJY8sukxM3hM)Z{4`S7dP~L#ElpU0k z0t8t_nFpz>6~E;5%`aS@uKyz3G_g`lkV~d7Z1kZr!P?Kuc3r{^0vkPI|8+h@b$kF^ z2dh_*e#HqobpBm+g$oBVe%;N@+yp8&kO#ymYfJ-js;%v)Q+}MCg^bFaqMfaG6)q(I z5-N~rdiX+>EL{2uuk8EV2xtG+7pvOhKT~<8KL49doE+pTQCVzr9c}CGB99*rQo8CG zl9abwWd+qP!8fsXpL>$bjBQBj(c~mm7q6Rk8DU>vRp4DowDM?qkHKIVVP6w%et=<| z1Oo=N(p}9hx@cN4vlg~L{g(F7aByW+c)!8gKF5c)w7J?rVwLBR=6kj&oRhlPe@^S6 zYTh@|0~%=CBM6j{+nGCEK%~I+qOJ((S9|GG7{#au&*P<bn?0&ZCKAa@N0)r|R2K=U!likonwPq4UJVAR!800}VY}?Meq*AvS^wN)4(^__7 z6bQkq2oBowFkLFtZX9B2QCBFUQg-zsuxAHsfdmio2*^G|G>*ZyeQ*KK&#fB!IvUS~ zVR9TLLL=m^yj@yLf+Hh)?VLh=!s$Cv8U6RF)ZqO%>`jjhMP6{QpV8UAgA{`mO3L6paKV4T z*>NP*eqwnehUM!}Ql7GidyrFL#f;rBv3VTcL6+Q;lJ|Y>kN_BEiw(?L=!s0GN;>!Nq%x!PvJsD?1xLOF45~jqmxR~gF_J_sff5C-P#g!$2Uan1 zo?t5o`6LOtSTkH07RHZf$Rp#$3<~>%c{N39KS4^(38>pXRhiCKE=}HM>#xkrT$YLZ z+C`5G|C^0|EDlVmazp$o#lOep%ew&S+vA*^y})^d_#J4Ut z4=#7CcsN;NsHbv)V~F&Poz<@%`i~j3jXT8WG0%|IcSE6tF5(AM4zleH|Ci(#_(=O7DerD>hsef%X6?(*ssPfzz=n=Lu!Nh4n-&No3xut*`LF#qv+wo zQUgM!^gmPi!<*pEpI;~Wm-MuqK=5;oZJcM*Iv=xT{9lld!;x*$g;mY1<2ch zI1a!`08E?Pp+t}kFty*mq<$RCq0Bmvmvri5@$-`j zn@c~wQ1fuxwvyZS=hCDr#tu>;tPz2lBf`rAUj0vqRW;1Y8d{i@SFbH`9zRw^Yl})x zKS}l}dUNDbZB@!fK;3ExVr6H=LX(8;K)E}SWvipY*YUpnxhLb`*6@C-Ll?JEN-#=i z+N-N$-@Re)TdSlj%>ww``hXu9NmRDez#eA5<1^JFc4f;P*mLT;t^M^i2Kf%fihj3v zYsj7vKz4A6h^O{})vUY+p&sk2Bv|LuUaP+lD|I6K-dOnz2OF51AG?;-+0(pE4#DBV zMO-c05j7$!Uv{>TtNrR)6$^h-}y|;hm$jcA&`X1p`xP8 ztDWHihkYnumx}M+b_w4X(gN+4WJHY@uP5P$qT3GsHn}lG-L{fLTh=0zU2#M1LO+iK zUjPM#V3C97OJ0o!a2%H+?h@6jMVKh)E|SFi;1PWx79W&l1Z71pxdlZ@03Ol^bKpjjXWx@+k2dw{Uem@44hQiqCtx2|)4{?a z)-;RX-c0u&Ih5MK;A9Mki0rFcPWC1q1xABwnWPr9saMu==@mSu$nA2xn!Nn*`EUCh z|8oI2tI4vfy#{KCwNa5+_tE;>D9h@xk=N2!>Ej5BH_=zz-_H+#Nh1lHcB6K9b4x?F zkj?n0W-d={-?^_l>$0P1fRYJr}5}tkEXB9{Wkn*d7+>2ldFYJJJ0B%s6^2WN{9yP^MbfEBj{p_5WWOf_AiFOk< zo~UdLB9UMPh$4*uxkr#+09=Vn58T&O&xy%oww~P_A-O0}YDV1P%KeY!P}MrXExt3~ z9L+X8&`a0S^NWJ2l}zbM;EcRK-k%tvkN>OwY?>lJm*;*iPoB*sde(FHttmOJRM-Vz z*@@`E&BY#ua=_Q_Bb^JBB%w>oDoBhs`R(?rri2FE3Oy$?&;>w)CQ8aWfG3XsL+I&J zX}H!w`!F)L9mDHs+8)}EbSkeX!QELkt6B2s-JX|!sa3X`(1;3gzPzzFyz~r#E*&An z5SRLbf~9K&$U8jqrv;7mKVM5DfGRSERe%Hik{pnL$9jTA(JG~#?-vEU(LluFG@DV~ zNzEWeshpi(2Y+F3foD#mHqVd1vzv~?P=Uum(%t&QWiOv$sl33w5T3~pTx{aWGBJ?G5N(E^DjQ|d zpLr89)c-X70Em0#ZiN@}l6ocJ9KYTBmm{Y8biQEW>!)F}7t|+`6;fw$XrC?V6L2-K^X3#4B?K;@81fZT!iI=*qO3&Rk z?q#3VyZ_~JV-D<=zNM#HfGXy`rQ-@JmH#6Ew#nuNR#x9VlYIm1F^T8*Qw=Rpo*LZl z!Tj~M$g??g?j0MypB2P3w#;`k2^6UkEdYHf;Re&a@XQWE*OT*WoRgc@o!`Z9issYYOrM@}P@Du&MgKUE0 zr)b`n#+)$_2kQr2BZYPI;&!a${LYQFHZ@Z zs#~5`&#X`xpw8N({|Y-NSt+Y-XS^efX(E9h7k%y*KAX}#ya1YFWqXJKwkS9#)Td8% z9Z+PB_2CFR+w{fQEYl_KNu67dcGeAXoU*i%Hn5<{&vY9 zJ8}7*8tSK?LBNuy;n!nQ4_M>bLKe3v-h|JsLGy0$Rk>?6wNBDM-3AmjO~jeOR^GUv zU&}`^Bj}BiS6>QaW)wjbi#$oLMMZKrSAAV-rAo`CXr>kb%!6GmxOaan6j>^T^(dIw18PnN#jxQM?fOr`OWtNcSRz$&Jyy|11DQexZI#R6~HZvNaDgYq6USb0A6uHH?m}HDm{a%)wDd{=S&kFKWuKZit_iOTC z1&;HA&3E)qD_NG;_J-Yntg7d2x31iGl|swkt@{2hj|2S3L#HYJQ?sRg25L;|y+f8k z`COjo=?A3JmaOTrp5IFm-_bRJ*y#w_%Qa(h)Rq$)&-V2L%-tqob!L>mW8N~ASC~qd zjj(>Js!D_t6{MxpZE3wxZ6u#`ckrikgA`vgelvy63W7NH;agkSf9Qt;UG%x6m>EZ2 zZ#m8LL7Np%8CU%hbnT4vKsej%ihijB`?3o8e#go3gdZ=4)6B~Ihweed zoDxKJ0xPKE>J%WMB7M{g*$@@0fN)tsRiMrd-h5#tl+-^5va6X+ep_R)xUivsQk>%^ zC-iPIquVrs*yIwJXL7D?rQ`zEG0iZ4x{j`wn#1!(^{S(0mlI&EL)RXt|B7Ry5K20o z%KB^=;wwe+u|Myg`@C3FDOKn4l$b`z!HhHaIFg&-liRnm7M-#ni{ww>n3W0u3^_gj z-Iuu?oHc3r^C8B*Y$F9M#NJ!eWq}S&Dl-|{q76i}-6CFC@kEl`aYB(1Pygp`OVIaG$|2B*#c*r}2GtX=ft#;288Q^Q|=BxFGAQU7ZFZhp~%+)*MK zeYMCs7&6;pnYAYt(pGl2l@GHp5p@v7v%dYqkNa{1`mUt*#0f4EdW06aKIFLrUoC2Y zy>OB6^P=X6GLhQSQ>z;B^E%nGST?yZFs3%(37-#0{+)kWfYm%Qi{qw%=kn=GVW$Ra9=;@NyTjL*!Kgb+AH8e0dqWt(EUq{XW z?n%4zo>rF>uo|=3j)j@VpJ6~3Xs)td_{~(as3uU#(MQ#3ck<_#ZP}cDG)@w%XrU*| zn)=2q?D&(Fq4eJE$>HHwRN|I8yzJY`E0e;*sq`j8s=o!tv2g9Gf?Ffil?r*;=a!7Axerfp$CT!2$x!?__0-m2ehcaKEJgKO?Zj?CSP2qoh85vZ#ix0L=1?pv)hHc6$MyB{ zc2(U1M*?0DzPGchAoCcr{}fT4L7MZq7N1f>mWVBjoA#x3U$(+sSzl$?(|sU~W`zT1 z(*{j%dKU0^=)>F3J%`?$rJ-LtTTC89p$;8g8_in%KB}?o%ICW*po{ZV47+6~^h#Ld za-PTA!kRr;%gzTRpGIfWXmw&zxVn{GynhX(YIwbh?~6pX0xmbN<*G2ze~`b<;s9$* zEQowXKr;SqX|OJ?eqiL7BJ|qlg}yh(bKUA|^Ds*)1MrV>{IadCsW-nL^3A_L zZbo?||1qA4N1lORy2o6qbrJ*n`X9LU-j2sI2flU9eH zu1yM)RsCnm?;8k){<|z;qqDfRkV}~hWYNU2Y&fBG zMNLKWO{p8!X)^0eKr2P}Jl&nzU{F!vE<(YN*|YG8D72*&9knk!8;E5lNYnm?6IWy8 zwsbQ|M3_cuK)}`a1?0RosOP?on%4Qcuk@#DFJ}*y?3|3I%D~#NqRqUC}M|G&IEciVWW?LD*px&Z$5j z!J+_Z><1vL6QSlnsYAeNF!!YY&+Q7*Yss zt#_5aaW)&(T+P{M0hc(m@ykD-w_nKPtulfICerWibB3r`UbY-_9(65QF+QRF{SL|0 z`r!>iqZls#C^khh>&}`r=)dvZW~bVvqRg2pMj(2f(WzwyB3 zU9rQ%CAKb36DEUR0qQEEA=ti53$V>cSU|Za{(eANo{YFA`q!N6#)(9s8WUz&9A7(e zUX*YR=aeh7j+-(0N`5a5sbAw-d_UV(_~MdKoyDIl+$S zko;lVoN$NYO33osafzf$Y|4(*lfubp)J8&zl(%woPQX|HgG+jR3M_kNFXww#EpfD1 z7=jgNTvYbvdE@RakMk6R)X^JS;FD1&ZM6k!f*vXVb=O$ONziaTggLc>kxLo{=*B8< zLe{9uJ#z^DTwrTypP3pN+dZqkBU{-aT&~^%`Q%N-_lorw=;Uy^^IX})MI33LcbEhk zV49JvDuZ)rgGk*o1NO;Lls{tVzoFj9Q(T_4BfYvQhdZ+Jk>+H7tCC}sdC^e%S`z); zjbhEksSMqlNC|T_Rp#rg!k^beRp;8j5;`g8bHfxzG1DLu`ZV;WRYv^`YQ`cj8KCQg z9>wiEb;jq*qjWM7KDcU14^n}2$s5DkdN(lhD%Bed?nz^WC2-2W6)URFE6%Jd$tJ%-QiwHr503@|(Jc(4`;u6S-sQ3rV%n-Goj>x2zx5p8 zHX6HpU~1IC)AQGdAYt^CcMT;wTa1s)GZZ4es+P5#`uZDx`lI#^AbHR93c~g4{dptv z|GxHZdLix{*X~A*MHar34SIj)V#j;jOfxaPw&$D)@9ci#2mt24 ziHe_L*A+$?HZE}beU%h>ZyC!vHFdg^>b;`e5%G_bZh_>7xF<_)X*cpnm8%t$k8nql zI;q70HFeaWFraV{n0LBb-L`GJ(gJTK0^GbTNb9kDZ=Q*CB3ESUt#FZ$T17f^n& z1>5iD@&u`K4Eh2r^UcmwC$>QM{08cuM3T7{7fkZIBol7{pNNd+@~j);B~x|mcWut@ zBO6^Xj>U~tpy>xABPN;140dtJqREdXlb$6Ld>xnGJ~4BQEkJK5>K7FS>dnzJ4Y#T* z4cI#j;yL^e4t5?G%=892I}pG*Z>}iX zy^oD;{`{~mf3^2LP8g;G&msg^bVRM9^)GF*+$qy#u5aW!PAc ziIptf3Iuu_Hbvq~N#%8X!eK$Hj2V)l>>h_$|3k>)82jqp;f}fxUV7M6(@3(Scf#l>E}Z>M&b>w72Qplaj|g{1w`c1mU!EGQL__ro zvNJg`v(gr0-{_czP#$^$3YI% zG?m+N zXKZDD)WFd2)b<^FE?OTF*Si9Kc;CnZPmnLcfY5=>|<7Jj)&hq{yzB%cu?khPHqApERN83r>Z% zp{}L}fn>$`al_X{mv;%PGMNOc+1FKpmQ_B5@&)6C0sdnR2dLm&t~~x+&_w}${se0% z6q>akKMMMsYfbB`i{WaB z(pPb`w3>o0DKHf7X#3NXGzi{2jIwY3sY2Kxg9^*s`aLlgR@e?d*EmjwgjH06JSxF= zn10w1@h2af-0M#t?(x{5^|NDYGwWng07yG`xZ}2AjbG6Hx5rOvA`s}Aikbc*I(TTl ze%iPV-SpiJ<4qAX<3I-I^Os>FO44c3~WWZZtK$@?yRly_~3yVO;KK+ydBbkG}asL_I2JS%6=dfg!iY-Z*-aaPio z)S4)+ugWTs(=^c|m&&TY*mDWz=@cgGgpY#|;dBegg-#OMrtu)e8J}-Pyu?a^@|DK4 z`X#T@NQv?cs3&@@BYJ>Q+U+X|XBmVBq>5CNO^T4sOeFn!`lRZ8huiQOUV1}sWBY4o zE9>eFt^R)qP9Wkwo0ghTXb9xJhqTZFP5Hj-d!z@RBtvk&bn#U>69pd<$IOw%mQ6GZKx{R7+VQ@^S@5fGx66ov9GCOw`NoR~o#y$Y0*JGq>%SZ_SI zI{2~`KX21{Z~z6AzsiaaZPf=I*Md)Z*|pTJdI1-WzRjmXMWsX>iP#L@6aJo_ZI zO1}-3yJz&~`HnS21o^tiQ>33kt!$U@U{|h*$0>f(=VZzPSbo&F+17lCtmp#cWt3OI z{C9y?cQ~B&Q}Q!3k9z|OepNv9Lhs8dD7e~h4d_>$|G+Ve_sbB9P)(Q?yzvp4b?4w8r6m<0wLG9iw{+rhmZaBm$sQ+H4&~ommpOT^QP4@rOgORR zl$$CwU)8R)kq!V#*@p#Q9^T7$ZpJ?P+G@%Qe%xa^2V2BUf8CloI0{->eSKY$(>q{7 z+hfIKLo$2EazsLJcT1DKcfhy+cBFN1z7P>LUE3H$2nBdJD#!6K{!f^jFE--9vkVjY z%FZVj-y@oLTinZ+$cqMoEM6)tbyVe>K6V>(`Z?AKmG<8Z&rQd>^SdZZDEyvoGFGLw#~f^1A~&&g_?*o^Kv*&%qt zI49#RUkI{dXcB7>mYTa)Ulw4%$xW9`fI2jGF+Bi)6y+~I*NLmKDwrBvT@K1(_j(*J z;!dh+ZG##t)$Z!`9yhX+*oz)el>LN~BQ>~E-@n?R`O15=TeM-T#`7rTl_L5AwTZvF z{)@`VN;%Wf<3?)avO{-FWI{+}{v&rxPLpvMqZcYVcc<#Od%}(@a*}{j0L6)N74y63 z&W;dfFbXI_Kj^fYM<2JkjdVwU@1RI?b91+((r0?DE!273>FuE^s&6Yv#aLr^_ri*p zN>u5sW(PhZNZrJmY)P2KwOZ65Z=0&gWw|sS>xIu}PO!XYOGEMHsy&95?~+M~OLsxc zjJ)jd4GIeH`|Xda8IOuU6`)}PyZWW+%K($7EI8X2bUqEy;e;HxWD~XO>eHfoo~tCL zO_hwcWY>ht;thaC!nYNv;M6{c^<%;W^8t9UZ`?3SXo58}ztrk-J5qNO3?D~uz-O0k zdrZb8>fSphMZ3Z{Q9dV+Z0!xT&J1XV;oarfQnJKh^o#w*qfM$tpY7zmnpx5XPhW~- zs(l4c`S@`36Ue&vEt7qfq`iVy8v37sT2qw!g|rXT1zoYLS>Dczlbyu#PLhF3K`)Jr zj3yAUk~dzYsnVVz+vXM=ATc@2-j~`apgh_WH_%m8Qu0HwV7xeB9h6QceZ+wP73(tK zpxiQ1C7?hLj_V(WTYYD!oK#ft4_Zp;;aeqEL0_M?EW|6+6#r8`qIR&tTrDr8K6d9s zn+198j{8ZV0VC3-4ch`b!#u8NB=O9ivcNqEFrkf8GRw-7ms$f&yqjD@T$fewI+o>D z+_56dZQXqB2*FC(i}vgiKYGSEx?ozb9D zp57ernQ*%4cyc1(E}}*U*H1$wCd^1Tqr-p*6oV^NS<~mS9F#UQktMG;H^HxcaLM@a zePMnSrz@I|D~ao2rIlU?Snjp2CRgOe4kE9uq4>fI9(eJPyR-FSw&fnLUEcElxI;A* zIs1v%{C=TnOMi1kMZ5l}it{~53(S>^S*hWbyd~@C^z>S1>2%NUC5os_GFl9IF3nJv zGH}!`ao?Z>XsA;5rc6~Ki?AN+OkN0(SGG~BHCjmq3RHnuXUiZ%qX~U}71IaoU%yg8-S@%u%=EOO z?p3aFd8VKIfv||>uT$hr*)6iqXZh_Q<$PKVP|hdsvu6I_LW^YD&T8YM*9wByfKu8P zuhXwM736`Mt=kt6+zyfxo*NUod#M{MFT{xM7NQs`PBpSvS~YflXL%_AmS#Cb6WV~{4|bapyZ;WDtb@h8l6B+tA^Q_u2vBf5 z`VsjyHLd`(FSgW$13rE#Z`}Mz_npU*duD}<9@qQn?TPt@dHZ7aYd-TD({VTtcLl=> zP3L5EghDPav<_Ydda71glU;T^lTS~QhK;2pdw?ditMr^aj0hsLwvuk5o?2OX2W)a{ z!*0cHYWYU|(o!=ZJfPChqbAVnwLOBvdyzO~N>eylVW~!bw(coyOBW3uej=r1sE{6r z|0X9X!`r@ydhS(B5*mt3@I{%C#-Ko-CHu_u682!jIm?@8$#+gh!NWv&YEYbV0@P~E zZzx8H;k!4Xuv;!KClJdtvrpAi1@ubB#_n?o-l>w_*`>bg5MPJ)9^&D+ZgUP65ua79 z+VfMs6``LJ2Raa!XB_)|tTVK~wd~~ZYA?`12@qnl{2hApYw~%X{T=U4c@te)sV~R; zIFNbCP-zl`)YjM(Ezw%Hw+`|$3b6aZA1KQcJ6)^Vsy0{%k+3z!RVNpsCZd3y-Ct&> z*A+nTT49sAF7vX}rDK*i{Ra<;0gWD*N0?HeS73QI1K?0PP2J3a$-z%A?*Epmj3p`% z-ntMD1gxZKTN#>wLfRXDixL%B0CO#vVYH*fFQo7i-I9;;rgqFxz)+ z9*4YiP)Ads!{zbop-8w1d}uAXgW@o)&A+q0mPWhGfNTOuAK2$|52s&Ck}|gaqytbS zZKnrxw|pjebA_R85U;ov2cWq8_KXTP3p+Rb43nVfU!aOza!37}5tudqC<;dz}LvL(2g1QdyL(m(L-T!hVi z_gEdab{ZEN1ZttnG6MsgdLq8K!@qoJ>v_uT5|+p$H7mB%?SU3x+YI-n5^Q;|43dTR zqQ>QbpmM2iQ_Amw**fvlWK~}}oojiM9#+%XgY-03hyaH|RD{w42@q`C;N1@VYKe6Lm z8($8%SKqaB4FWF*6#$%Weq&JFfduSJ`sCN zyPRR#Y>#+|?5u^&XN?B=Vx$*=*wv%G7|e;w1~@^uFkRp;?diECs*ut7e)>-OGh;u5 zOw2VxH=%3isTcGb{K)`3ETkpo#4w?iZPVE{4>>H9$GMz>vsA88Y^WhEDrJBhO5y6Z zyqIZY-^da$KS7uEueri&0V$PDp>9H)?~plyiW+4r>nZUy*1IRhZT@pH3WJ@>Nl>dv z)i>v&t$2G+t5PZpz8if5nL7>p6FPK*;29ZBe7wl31U&qc>+zCK|6?f--?~CYp$P{^ zxcbK4AmtCm@|)5%Rx^1k?2K$0fHc|~pdY2k#@g8LV=dVA;>Wie;KwsO>;{VQ*BT3c zpuneLQp~cMX}^2FdAF8rTp}unsr}_YKht`2V`HZ6czyfKr3t*ww_7?HJ ze&0?w?GruC_||Ofi%_Ahx%K{>qf>u$hl6PzqW`z>_Z{G7i_+J`cbIXik4P%a>DB#! zb=8k9xzc?8!kUE{-($S36=#?;8 zU@g@@Vx?k%%ou1+G0Y6;;dDx}@qQeOl^nNFIrwvjJr#{THD45sA8-cx`2V}=z3U#= zV!=UnP7)MJ1}w3krqwnMq$-QpY}~qMs;pj-DkFSXWsRTvFDhx7zTwoiX3Y6NhT-7y z^shVEyOe6h`yPy;(kkS#O(C~u$5&Vz{Uz|aWHfm6v}RG>H>JB>8}yR<8!6cNSITnD z|KQR8mc0LwbWXjTm97L*-JN%k0627anjl>^5joJ(n8NEf)c;J*(*AAKz@2>Rsh}DZ z!>txWf0ggs3lz5l=kDZxH0`qsz`G)*j~?RWm6MUMLBO)j9FL}Gd&p()SX#o?Fj4D4 zvU(oU_ErIH%t}}~GZi`S%=3RP@=&u5hsEmZ#@wshlEnaGRW|W%AUwcokV;F@mMX5l z?UDtUa?4|fjlz}sOaKwyLc0I6$$&kCRgq~^Ws*2pkeDb7u`I2>9WXZYtZ*F7|xvre|}s+iv)5>p6VYn=KsV_eB*9iD^dO zG(&O+ z=C>C7YIDK2M7nyi^JpCfZb98Il4e+U)BK6BRc|pM^GncF&|HIUCm`bh4PDa)vYf2v zDytg$PWkFv3cq!UM#4(~&?@Dz#qDgD3m(N%EzcQL5EIlD^qVmL9TI8K*DvOPJ@|%3 z?(#R6p~@D^>ibyU`mX^T-p1%_&SJGk6!E6kA0gjaxLvJ4C7apk=?%!^2O_;4b1YTA zAy;JKo8ROKJEJ-wOF5g?UM*|n1z)|9V^#B0@Kmy5bm|5#Do{nyJs()MqLFpTjST{icKH&HQud(`=Dl#>3qA1&N) z7WTkeGyi$^ANPppas!p8KFiek${bNU6yYz(ZBom69^zNEs@~pdCqPvC{@q0{QV(s6 zRr4;2J^Yx<{%N_8Ia$nQ>WZhFF1pscUR$Y6dRLAxV1IJ(5vYo)FV$Xq?SirxVD z2hiw8$j7o3$=-n#l@4dbP_|&ei~N#_o5yLi{9#hU>MFY%M~l6kfj>$z0}ISWeuAz; z{KmSMmAa-NV{QG_^oDun20>6fS-dBTr$zOC1vR8P7C}fj1pSRjvzt{6x?gZI0I#{T zi4>(Q@_Ax^dMEvbOU-9Md9`L5E%Dc;%;i^CGJ;Z%+l#*`oY2_3WaaP6PD+XbRg^u| z($u>63WnvM@9#2S6#48~v!XMzTW_lqW^1v={2h$B&#w@yr{_}XaQdByL7{y1$DNx@ zv;kmidH%}pTSvCG_Foz@EIOGy=7Uzf<4^X0z1fctBuYTFCmJf^4`uo>w3n|8BFW{6}I>Lf12b!562oHN);*p~bLg_XDQ# zh)ioI^^wGwURob;PXOCnZ{7YPb~epvqWX-G5YI`#hZGb}0BiJe-KA7=8L+7ob%iGY zpdZ`1o+>?5)0fbl@*m#kz8VgVd{-p3G17lyctaLfE5<7-Y5N-W;79ExB|Qc2+*`_r zRy1FAt24k5gWu=imdm-pwTKmAbw~tz@|6_4*e9U${Is@jEUk*(S%J<1jXs~LS+1#~ zzjWy_pj$@BibwzXwQVM9xRe<_(`X;S*762KKg#B;)tUgW#?NOU-EWM5W1GvQs5GKD z+cOozwS6oT4gdR&l}$Y&n5q#Dx9*x{`cn|S-}i|kTd)$S$Bd9S*&^ur-JMB^@|Whq zElEV5?+H-`zk5_RSpWmzO(str0PRHh^q#N$A^I^|esKuxL(LxL9dLbnXk?-x8Ld4| z!rZDH+~S2lZZ7-rKTPXZgl@b12np=5l;hzt**p$Jt5?9B!MMJyqgZQea~E0qy&J1i z9utJr5+3%=I}h6Qj+_Dy>^!*MUjrU}0ucPbF|alE9lf>145CVPNPus##mN51M^Iot{`q-A6{0!d$%S%koXIB;$dZ(%OB3kp_q|@vnfW+$oq8f^$JzjdX)oK>W8^L zd7|Z$^iHFbA~!(SbyNqo1q=agU97V;yQ_*m%@oVdjGE;F7z!k|PxRca*XnduX#{6N z36Om@48Bl@j1kag|MgZ*irkRI|3%k#2SWY-j~`K_>{9lYgp`qe_6l(!XBB0J?9C;z zvRCHWBP%;+HOz#w%Pia3d-Hpqyg#4M_w)UI@4s}f=j-u&?0v5M96sy3&aY(@MKhbr z59w9h*tixPg|~lDj33HZPv`00?MUo1{cpbRt4cB;Xbe;P5Y1=AU% z!$+1KfSbtuYhC?x$fG31!#YDiP0=AgVe`Z8cuz9tjc1(}Jm(2DtWqx0taU2bk9Dr@ z7k1u0H3ak21H&2gVQ9cL`KTQb){4?(vy?lsgV=hJ|~5;d(_pwqN@j4?DGZJ=dnW?(;WT zx7rPp&h_)!${8+nb2(IZf|Ek8=1){hK6s6rn8hbhp4*-<_qM&=(5JkGz{1**h}ibp z*6Dtoz{+`l#@Ty}D=R@%GG%DzuysQJ=O&~dt(eBd>ftB+p0t@J8 za!C(uWiPIa;kye|sgt0&#^T3H2+z*=8?DSsdQ*NCf=;WQSBrg9kLm zyg;nq^F&RI?=zQWMPQhYmVfR8@NQ~gc5}1}IOtbQu@t1;YwoV!?JMRrG7t6mrz-@D z*^f(yB{`G4f=j>FUV$mzs21AOMTYv_KcFB}*+OWcVj15VX>Bmh+j?^3k#AK0nR@p> zeI4|#jG8i#U5s~CRdsDkZ_JQ5p)WVHv@-Ut6xZMK>$}V0X{hoik+3*gVMuH}n#2v? zF}t`a{C8rfzZLr@HM4s5|4=ZNTR?d=Op1Lh02tP){As}{^7nCWj@A;l*)Vl?d`CB# zj8CTJU=JGyNr|qMKEO;hKzx-Aucc0?`n)EMzvZ@2-i7$048@u&v7% z6SR68{u?~?)ux)LV8(H#bb$X`01++NNC!Xz9{>u(I3&POV8Z5ABo6eadGb#7 z#?L#8zmmW)H{e<&b?eS6Gj-TLUkKz1E|&2jfp&TIukSqTr7f+l-sFV%g~r7e#I~A! znHk!=2f_KKM^ec_H+_x)oWB_0QyT~=`2?q}+a~6{K&maG2?G!0%v9H1fyUpe*?ku2 zFzlc4XVc>A_|LYEvv+-lFk&?&gmDLt&q5|=|cG!~{jK~@I&|gah$^6`W z=?n|LB!7tl2{hN#vL+WH3OT11g!Icf0KsFX(Fc-KCUcCv?V_uf<)GDt6Q4UEjzL{z z_C0rBe7IG&GLd1k^#DsC@G-#qdsPj^=}5Hbq~d;G_xe2unanSia4y_LStf{lub9v4 zjlJ(Do%rY2K@pq*L7Mq^Yav%LK6vip*%N(BH5r9wwi-gS20f{c5%D&J!5hN59Ew9ni#LCix;%TeLv|2L0XvX}nARtK zTP1FPW#V5CRInGy9bP4Mq8xh_vhhj$;2X2y#k7uouf5WZ8(b&5bmy4DJx4CzcaL2y zR+X90R6zOyv+O@Vow3scc)~v9`1}P|c&TrJ$J)I!pa@9MM3%8-{v%v&Y~GIfP>3pU z(0T+2X<*i~jk#djzf61AFLQI!uc+c3$^XCt7-de4b6UKH0|t$+or3`heDs&cAkd~h z-6vTD^N)$V{qetKm)A*Gu-W|icJ^XM$l3^YDw|XWt1NES9Mfx@uH!wkwVZqbVNPUe z#p7?*)@=rybW?;E&Yi0g&gWkDYh(mSF!uAC<4m-n@1R@<&)wo58fRzwtK%?cv(+}hTFR4vy%Lo z7mzFITPLaCYoa8$&qNx6y7-p?Ssw5B7)IU}D7iX?)i1=<>^{ZGTq-ArkiVP(k$nAE z_WhP7^wSh$f%swxiFmVfLFs$Vy|E40xe$2c{^cM@)v$peK{_!d1)TEs z(JLpCnNnv*L)w3UIu`*1Cf-Qrx$?lK3}A{pCWHEW1D0o(HQghav6DaEJd=3n?{hhT zwj1nn-OG0CAve6pXnE_?k^7K|+rF~l#m}U9>fRrw__RIyQZg)d^#8fjIdB9(Z}%#+ z=QHp_dX(jm-oc3C!{02}$CdH_3x7^)0aXp7y;x8=klOJ|i1j|}Tx(3GM z1d80_ZgdvUi6#be{Pq!GOMsyI*1rgPdJbR9_;@h^W$>BBV8lE?Jxw>)rREjcAaW&y zd`oM6@40fPZb$yV-tsU3L{K`BS^GxTNv1jw^gK4X%6paNAHmh{`uTR0=+K*S zb7=!Di~X$O{Kae&rT)IUTq=KS39x+`aYrE~%_cN>DH|NA&hR)?+ z@+wnQMu0!2S1$dFKbZdFAA-osv>vjNgX(dcokI#?lsf^|A9Ug)SZK5)U!KZRKvlVL$ zf03SrMUz}2?eU2M7D>M^RBY%<#1B(bpW9*DOXuIC)E|?HWalmz0-TidWqgp`;1vbp zDYPCr<6vQCNBEaGh^fCAejhU`X_TWFU9~I<&VfC8wZF~0|8pvN*befPKOwF!#(}fa z>x|nWCx6|b_c4XY`T&7I|Qx9b({U-cgy z^=v<3(PXNo2K!7N^-_*$l$4Z#RaJr#1~R(32{xk2Zf-U6 z^Yg#^>ZNyCI60MmBnheiNVu=?a->R?Ur=zvg6vJM$!^TouhJH^KZr`m3+TdmmB9#1 zMomq^@~`{4y1K^42iv{7W-FsL#zVyh-qs{otUtZ@4?r#_u~B|%l~4SNP;UXVNa@|`g#aMrd-EnI3)Lj9!$Nv-vm<^XCr2i_#t#{$iOAVhDI3lNoRbTiPLLZ z`c=MfN7`EW*tqO9wS{37IOF5vG751@Xy2jO;`M{N^GLm@`vM!%g&Q4ChFjaGkn3*5GQ>cAU=!n53lc&4ztD{5zb6M3qCAfgF2rIAS7>XJBo$;SGApmlxG z=oAiX1lwY=m-lu@k`Ossb4Q0VhnQdGxvop4txW<$`Yc770cX|`dgq*_NIg7iSto(Q z^ed_g1H`!2Tz#TLhF0xsoPE*vM>Wu9qQQVX8RM=RT7Vx8hZ;Hd$bJ7bV0jHJA;~qZ zRbcRXoDVvs4PcpqMIWGq2|T^}piehxL-AdHU>uR(pCI(^6L; zHqMpm`8J+~KB}c#^0baMAYZk~s-&L%HWycOK}o~yP}p@I{k$K$>}f$ZV<-y@Cnu+J zXEcKSQ$+RvN>)zJ|G{=YOrY{bN1R2~c0lcFgWhnf`0nU873#d10M8>zl+XVDzHxt^ zMh0lz)>L%vYsL>+T6&o;&sAx$ZI6`jV(nQb^sYD9~3$fO9 zg!4H$?4fsZLU5}pDsDO1;wEZK73_K}WXCH{gh~};l2-(l1G0?P{SqO+gwWU+HX2I$H!Ck0-NB}y*<~?QJ9*r9Cls-PDhM| zlCAH_X#8X63jMyl1L+_$`j>piOZFXFbY zxF&0kB$$KNRyvVq7|vD-bblr|sZ$ea;#35BLO$qi@uVrdyOI7Yj^}~myPNmr@2gc$ zT*JMFem&bvWi(wIx26R>{kbaKDF!go<-(e>@AYZzD-j#n7`=b^SpbG8K6e`+JXx+? z52miH_ruhc3h;!-tFmJgd`>oz%__Y0tja1X`#F~@!aI0se=IAj!Cx;FlUTBTbY6y! z9g7=EoE|w?=-D!N)3NsDg+dJWLRX`u3xr7MM4E&yu+Qcs+4PEClsK4YvD=#0$PeF` zI6ZMu?F7=5+T98nqj!D9)go8z z@V+D-1Exnfs;v)Re5YLKER}k5aUm=oZiBX*v*^pQXsLBs?Q-G%Q-Cnsnj}5Dahg!9 z_hu5+a!x3MF>$-8L3D9w~ zOd2XhjHBt^qwXD`#VywQ(=86ZC9W-pbZvUp{+T1Nxhi5uW{z@u4=iW`R3kFCtYm^c zd6JiZd^9JiBas3{)0o+Z4m*DWOjz$af48%4iqPD5e2HbfXk3l>*mH7na*zFOdyme7X47x5Ci}EAw}RC)ryYeO|Fh z@_^=vk`Y7C_2x;V(}Va23t7>QBOma|g4j?$sgN?li-qM`vEHoy^NWht(0bK2*UXS! zRqU(_GgFLGZHp70o1qm&lDLT_jM=M1?G@lZwMX6NQYN?h)Z<&2(k|p$91JN>Y(|Zo zw04kPs!z!Det3mm><&1 zC~je*vC*^(<+S+NV~cn>VB&2qn&VdgD;#nAX+>3ovCaLe5u3h#_iapezxQtKgoWk3 zhRgG8netY%`^;%C=2dx6wwPtk#^Yt%Y@xMxZ&|-J8OSsf6|8IttMr-%C&nq4qzikB z+J2GPKKvq4s=fqkEXoC_JHAobJxe^4R0;7T{}+DxI`s|is}&rKdv2;~XlZ3HCGphBE#t|D(-*y~$*il>fBxcy(&^hI zpVPyM?|hN3cKiU|@X`+s9e;nuQBBeQhV6G}=HO7cPwbgHg?=`}hqMVgGP2vp9OlDv zEiu;C))GYK?p?i`v8+|@$-6%6{ox~hYND#|7$glZE6s^}>?lqq-M5=;npzp6>Hj+C zPi6Q>Ix1;LrXXfus-$is*i@7!ZNWQFqr^VPnZ+`XEAD1UA0}T)U}reDJEL zNKXh*Q2C8*$4oH1O;fwXkQ>ZyHRb1~ez<8=^)kU!)Mu@d<4)0y#|)B$teAae@s}Q3 z^GyEFg(~Y9%gW2Msf+p>fNf^ul*<^QktZ@o-AXt(+$u0SR>o`+y=%59526g&~A<+5q%;0b*Qx{kz23Id`Bt$ zkn~oA!~}~Rls8FC_kK8%WdNOIjv1J;oHxrGC#hd}&Tom+>5uQrNZ<>FzIla%(;298 zhsq^Pc;3PMWc{B!k$UsuwCM- zr7#Rm2*0=Db!M`ne#iY8v9H;{dPNwk!9ub#i=`0~R7{!|!JIC0Y%pL#X=y7Z{$zlm zCte2keM4*NT{x7&kxX&hE3Jyg&CSh|dY&0y=LANER-n2wJrn3V?)S*&-fJE>hmSh~ zF9+q1*Gz?@inTB9VI2aNs~IPceI>*71U4KV6OXlkVLDlP`L|n2m!c-L3$)(R2&swf z{50`&xcXXR;u~9!b=Y8OdOvR z%!{skl40SjYX-gslN`AfFmsb%E;01?@3qd2S#!aRj7s$HJ|u20Z@*H@_r1+eS~9l7YuCx`^KykN<02a+47+!s$kjjO=%d4B&VFrwyow`% zZMPi5(XF=`2Zu80sbgCU1evc9@}ziDb->;ABO)-r7!))j&%U3mw2seQzm_+Irfo>6 z#%h(P$e$eN3i{aFbBsf2{uPlqd5fizT{~NRV1NH(4NXnvx5RWGDCkr1_=^gszrK-< z$|=)44B?oF!}}R;_h3cW@9m+=_?#Q_@YvaKdCYG$Ped8j8}9 z50V6}!TuJH-4)NF%U?**{kNOP{M&A^X5^n1xoe3Kyqj%S!REGl1nOw*vT}15$A3pU z%qEywHsrKcU-9X($59m@pCQuwt_$q#`E~BeyV)v;|s+akn{(ER^Wp zrg$&DqqMZIJk;ZI5&c||#ygPnl_JGk2OcLW4`Z4Zy0uIRddPeipoi3fO%rbm7(#&z z-;iwJJ3NH#vZlEy_P4aQTJ5ko)LG9w5)J9?)wD=TvOy`qqIn{a27b=T{N zWE*YgKo;I?oDAmTAF*eW?>xpo!3Mi{TVx8d3YU=Nc8_??-6irEh_(a_@-)XXrhwc3sEVm4KlSu@cLG?U z=V0J0QJ6Jsp+>>Bq0MCLVtgaInK*y@;TSViKA{>gTAL`mI9LQ*?h$4EvS;o3osc6r z4Y(Ndkt_<97))w+#pIJvRBmzZwMf~IcokQQpt(dW8v`?x7$gx&{_V_jIqB-HOW3SSyJt2jATZaHMd zPW4*`jQd>{Bt;YWG6v?NtuEBdqpk2E=@)U~_;~!jjEYO_ns^ncT`n)b)Tsi*$yi3`suE>;`5u$O4X+-NPE1rwhZ&=_1)>bbj3)T2xPOS-%4r#qvQ1Hu?RNDul#!^5a%xkQvwcF0~`#6vJm(-+2kFQ zS3b*de)z{vBDLsxR6mQhYPHQ6zmqyxp%av=lrfE9(J{0S%4zWk->`NYA#p(mOKc0q z6zd1xMbo2unA2vE6ag&*tukDnq?Q;Zck{BZR!$_`edg=<`XS0S<-m^5xQU>FlYnm~ zTq2ub{%t#Q)4mDHP>>U@)7M&9;|c{5WGqUGW=w`c3(lA(X!-GA^=ACr2$_1Vfk~w? z;7RA4@xfVOb1wz#BUJj-R&XUceDKyv~RMtmj2tGG;HjwcoIQ;n5i9*i4d zSnn(8qVfi?CO#Slz*K0JRr=7puPJGcW>igiUU+3!TDhB11Prl6Jr!7tly^}MEjP`~ z8zfQ@UE*NZx*-gfNs+oZRN_)wR01@NHtihJr(wl=gH|0%a^o&E83gafzdygO%VHYF zbKG4S6TltTn`e$(UXEP9@y>apt5!PFU%}@|6L>S>EKu|d#!ek^+!U9HX-ThwG&wcF z{bi4x+u`|z6emCaPw0XX?9Y_MjV!K}-r=ywUe~j&&$j5Xn+)Oz2Dv7SBtJjA&>u%z z?{9H>{2VwiK38E%-s{$lrzajSXpGElY~og^{#%6wf)%-)4g22R;=B6UgXL!Gs-fDE z9RlS0sh{>|lVaT*?PS#>^k-YY%W$PFc!)i3BVBKmJkij}jhH?GUcQ`(2}>g(DdRv1 zvDWivGl(xBDK-HT+_A0wWD3k~FGR5N{=YFDjEGAg_*g-0e|mBp6~o?N;K18BT7yb} z3GBU)i|KE;pBVoJgqb0V%F5(4LN=7`?d=&E8NTX4x{6P(QBkQ#!r)$N%i13bSgOTk z`TNT3D>rD{P6pgS;tu*qg=ODvK=(1gn!_fgDF~Ve;}sqa>19L((p| z?ex%zxsxy8AP$$*>L(4&H4Vj}X$dr=JGO+8cT+-|zbl#hCD|xDF!ew!+fn@wCE*N| zG|cuHMq{+ZND`~R_)LG?uv;vuww&GsYjdqE1h^re#rse7ucJFd;;<>r|Kk`X=%&OM z3^qU{l{{LzFmVNvmT@pMWZ;!{sR2|WvH2WePn!%F)1)4}Mrf18KjASg}<~x+qa~;xcMm z=$3vTxO5;y1;Fy|OGx6fY;E8G1-GkiK3+yjAG@fq0n?{xC6RRmaC)1Z=PO?uvdmZF zA|749=}a52)mH|8W}GGQI`EgkmqES_!h`GSHG|tpHN}NL#t!)kNk2qX0Dwql)F9`C>VRaeZ#W%xPNI6{t|{8awd1z#5nazX5;l zw(LXl1Gf!iDOtLDJA{;&G41A=g(*!?q(HycJZ+bii+C)ye+4AwVf+t|r7LVkS(l26 zKo)x9=g6?^M(HLOsMYzZ69P3#Kwisa!72EJG}_NQ^~mg{EI{R_k87{7IVUMnRzZ{N zI0oxW5V^2^uQf7knt2}Ajmj`P*(8aIcV_0}1)NaEQW^qt2SC90SBV$`ozC1{QQ&DghWWfDw`@zs%OSrYypNHitAYITH6q>|EivIla$sV*2dlmtC z65ks{RduyWy-jR*0M+$%PDm4_bFBfk@Tw@fll<9VSW3s@#IDpntg~?sZbGgD#wi%i z=er^u-(y=Tw%_CkoiG#rFQ6!taa1Rxkk2FLuN>dm@1~N9m=C^*=eS(E%4M7d>eHYE z#ja8PFwfnafcL_0p@g^zpuMV6YF&j3fx%QGlN{b^)P`V+F24$B=sZ_#dhCezWd#VdE;oy{mAlU!!l!Q-B_Ix0Ccu#4RMnUm~ z0I$Lg7htZ3;XZ*mxpnM?iy_HI4-vN|aV2ltO=E+EoO{#+=%0^%MLR;Ewy3FH~mIw`Q2Ie9My8Z$#bpE3E37L2bxz32mul;g#4uFFCA6=@Y3zT`-er_D3Ofr z*M%2nJmmj4HF<%w{as!y-iArch2=d-i2O4vo9jDq*Cl8L0T;14U%;_021&*8XIN

t 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 aSW$nTC$d*!|I%LMuzu9m*UHnp9{e9pU;LK< literal 0 HcmV?d00001 diff --git a/docs/img/networking/img-7.png b/docs/img/networking/img-7.png new file mode 100644 index 0000000000000000000000000000000000000000..c0a99fda5e94a90d789ffdc513c95e42d11e42c1 GIT binary patch literal 72047 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�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 + +- RavenAgent is deployed on each node of the cluster in DaemonSet mode, and the container is deployed in host network mode +- The container network within the network domain can communicate with each other, and cross-domain requests are forwarded to the Gateway node for forwarding +- RavenAgent on each node determines the routing configuration based on the CR configuration information of Gateway +- Raven Agent on the Gateway node establishes a VPN channel with the Gateway exposed on the public network. + +### Raven L7 Architecture + + +- RavenAgent is deployed on each node of the cluster in DaemonSet mode, and the container is deployed in host network mode +- The container network within the network domain can communicate with each other, and cross-domain requests are forwarded to the gateway node for forwarding +- RavenAgent will start service according to its identity: ProxyClient will enable if it is not exposed to the public, ProxyServer will be started, if it is exposed to the public network +- ProxyClient obtains the address of the exposed ProxyServer through the Gateway CR and actively establishes link +- Requests across network domains are forwarded by ProxyServer proxy to ProxyClient in other network domains. + +### Gateway API +1. The Endpoint property adds the port +2. Spec add attributes: ProxyConfig and TunnelConfig +3. ExposeType support two mode: LoadBalancer、PublicIP +4. Support elect multi active endpoints for l7 proxy +5. Proxy server need listen port 10262, tunnel server need listen port 4500 + +```go +// Gateway is the Schema for the gateways API +type Gateway struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GatewaySpec `json:"spec,omitempty"` + Status GatewayStatus `json:"status,omitempty"` +} + +// ProxyConfiguration is the configuration for raven l7 proxy +type ProxyConfiguration struct { + // Replicas is the number of gateway active endpoints that enabled proxy + Replicas int `json:"Replicas,omitempty"` + // ProxyHTTPPort is the proxy http port of the cross-domain request + ProxyHTTPPort string `json:"proxyHTTPPort,omitempty"` + // ProxyHTTPSPort is the proxy https port of the cross-domain request + ProxyHTTPSPort string `json:"proxyHTTPSPort,omitempty"` + // ProxyLocalHostPort is the proxy localhost port of the cross-domain request + ProxyLocalHostPort string `json:"proxyLocalHostPort,omitempty"` + // ProxyServerPort is the proxy service port of the exposed gateway + ProxyServerPort string `json:"proxyServerPort,omitempty"` +} + +// TunnelConfiguration is the configuration for raven l3 tunnel +type TunnelConfiguration struct { + // Replicas is the number of gateway active endpoints that enabled tunnel + Replicas int `json:"Replicas,omitempty"` + // VPNServerPort is the tunnel service port of the exposed gateway + VPNServerPort string `json:"VPNServerPort,omitempty"` +} + +// GatewaySpec defines the desired state of Gateway +type GatewaySpec struct { + // NodeSelector is a label query over nodes that managed by the gateway. + // The nodes in the same gateway should share same layer 3 network. + NodeSelector *metav1.LabelSelector `json:"nodeSelector,omitempty"` + // ProxyConfig determine the l7 proxy configuration + ProxyConfig ProxyConfiguration `json:"proxyConfig,omitempty"` + // TunnelConfig determine the l3 tunnel configuration + TunnelConfig TunnelConfiguration `json:"tunnelConfig,omitempty"` + // Endpoints are a list of available Endpoint. + Endpoints []Endpoint `json:"endpoints,omitempty"` + // ExposeType determines how the Gateway is exposed. + ExposeType string `json:"exposeType,omitempty"` +} + +// Endpoint stores all essential data for establishing the VPN tunnel and Proxy +type Endpoint struct { + // NodeName is the Node hosting this endpoint. + NodeName string `json:"nodeName"` + // Type is the service type of the node, proxy or tunnel + Type string `json:"type"` + Port string `json:"port,omitempty"` + PublicIP string `json:"publicIP,omitempty"` + Config map[string]string `json:"config,omitempty"` + UnderNAT bool `json:"underNAT,omitempty"` +} + +// NodeInfo stores information of node managed by Gateway. +type NodeInfo struct { + NodeName string `json:"nodeName"` + PrivateIP string `json:"privateIP"` + Subnets []string `json:"subnets"` +} + +// GatewayStatus defines the observed state of Gateway +type GatewayStatus struct { + // Nodes contains all information of nodes managed by Gateway. + Nodes []NodeInfo `json:"nodes,omitempty"` + // ActiveEndpoints is the reference of the active endpoint. + ActiveEndpoints []*Endpoint `json:"activeEndpoints,omitempty"` +} +``` +Global Config +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: raven-cfg + namespace: kube-system + labels: + app: raven +data: + enable-l7-proxy: true + enable-l3-tunnel: false + +``` +### GatewayPickup Controller/Webhook +1. Select nodes to provide tunnel or proxy service based on Spec.Endpoints +2. Update the configuration of each ActiveEndpoint according to the global configmap raven-cfg, Spec.ProxyConfig, and Spec.TunnelConfig +3. Traverse the nodes in the local network domain and update the node information to the Status of the Gateway + +### GatewayPublicService Controller +1. If Gateway Spec.ExposedType == LoadBalancer and L3 Tunnel is enabled, manage an LB Service: x-raven-tunnel-svc -${GatewayName}$ +2. If Gateway Spec.ExposedType == LoadBalancer and L7 Proxy is enabled, manage an LB Service: x-raven-proxy-svc -${GatewayName}$ +3. Create/Update/Delete Endpoints by Gateway.Status +4. Each service is labeled ```raven.openyurt.io/gateway=${GatewayName}``` + +### GatewayInternalService Controller +1. Manage a Cluster Service : x-raven-internal-svc for all Gateway that Spec.ExposedType != "" +2. Create/Update/Delete Endpoints for x-raven-internal-svc + + + +### DNS Controller +1. Update the configmap edge-tunnel-nodes to record dns for coredns +2. All requests that using NodeName+Port are forwarded to x-raven-internal-svc + +### CertManager +1. A certificate is generated for each ProxyServer and approved by the csrapprover +2. A certificate is generated for each ProxyClient and approved by the csrapprover +3. A certificate is generated for each ProxyServer to establishes a tls link with kubelet and approved by csrapprover + +### Proxy principle + + +- ProxyClient obtains the public IP address of the ProxyServer and initiates a gRPC link request through the watch gateway +- ProxyServer receive requests and registers these ProxyClients as backend +- The Http request that across the network domain will be hijacked by the Interceptor and the header will be modified +- The Interceptor establishes a socket link with the Proxy and forwards the request to the Proxy +- The Proxy sends a dial request to the destination services in other network domains to establish tunnels +- In this case, the cross-domain tunnel is smooth, start data transmission +### Raven L7 proxy logic + + +- Gateway,DNS,CrossDomainProxyService,IntraDomainProxyService and webhook are unified management by yurt-manager +- Raven-cfg and raven-agent are created when raven agent is deployed +- Each node should be labeled ```raven.openyurt.io/gateway=${GatewayName}``` to divide network domains and create a gateway CR for each network domain. We expect each node pool to be a network domain, but currently there is no mandatory limit for node pools +- Gateway controller elect gateway node and update gateway status +- DNS Controller maintains the configmap edge-tunnel-nodes for coredns +- CrossDomainProxyService and IntraDomainProxyService manage service and endpoints +- Raven agent watch gateway and identify themselves to enable service + 1. The Raven Agent on the Gateway node exposed to the public network starts the ProxyServer + 2. The Raven Agent on the Gateway node not exposed to the public network starts the ProxyClient + 3. The Raven Agent on the node that is not belong to any gateway or has an exclusive Gateway starts the ProxyClient +- Cert manager request three certificates + +## Further optimization +- Re-implement a reverse tunnel scheme as a Raven project package to replace ANP +- Optimize network links and remove Interceptor +- Support raven l7 proxy for edge-edge commutation + +### User Stories + +#### Story 1 +As an end user, I want to make some DevOps from Cloud to Edge, such as kubectl logs/exec. +#### Story 2 +As an end user, I want to get the edge nodes metrics status through Prometheus/Metrics server from Cloud. +#### Story 3 +As an end user, I want to access another business pod data from one NodePool to another NodePool. + +## Implementation History + +- [ ] 06/12/2023: Draft proposal created +- [ ] 06/14/2023: Present proposal at the community meeting +- [ ] 07/4/2023: Update proposal + diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go index f732bdcaa9b..a1352c1b16f 100644 --- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" ) diff --git a/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go b/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go index ebb32758258..a62c4266aa2 100644 --- a/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1beta1 import ( corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/pkg/apis/raven/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/raven/v1alpha1/zz_generated.deepcopy.go index c736ea956f0..d2d42342df3 100644 --- a/pkg/apis/raven/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/raven/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ limitations under the License. package v1alpha1 import ( - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) From d9f74ab9dfe49060ededa7a5458a30cb86188ea8 Mon Sep 17 00:00:00 2001 From: Liang Deng <283304489@qq.com> Date: Fri, 7 Jul 2023 16:32:16 +0800 Subject: [PATCH 50/93] update chart version from v1.3.2 to v1.3.4 (#1592) Signed-off-by: Liang Deng <283304489@qq.com> --- charts/yurt-coordinator/Chart.yaml | 4 ++-- charts/yurt-manager/Chart.yaml | 4 ++-- charts/yurt-manager/values.yaml | 2 +- charts/yurthub/Chart.yaml | 4 ++-- charts/yurthub/values.yaml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/charts/yurt-coordinator/Chart.yaml b/charts/yurt-coordinator/Chart.yaml index 66ed1ed4043..e0874d5a1d7 100644 --- a/charts/yurt-coordinator/Chart.yaml +++ b/charts/yurt-coordinator/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.2 +version: 1.3.4 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.3.2" +appVersion: "1.3.4" diff --git a/charts/yurt-manager/Chart.yaml b/charts/yurt-manager/Chart.yaml index f4fac8040ca..7c795acc790 100644 --- a/charts/yurt-manager/Chart.yaml +++ b/charts/yurt-manager/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.2 +version: 1.3.4 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.3.2" +appVersion: "1.3.4" diff --git a/charts/yurt-manager/values.yaml b/charts/yurt-manager/values.yaml index b8036028028..f6d9eba5fc2 100644 --- a/charts/yurt-manager/values.yaml +++ b/charts/yurt-manager/values.yaml @@ -13,7 +13,7 @@ nameOverride: "" image: registry: openyurt repository: yurt-manager - tag: v1.3.2 + tag: v1.3.4 ports: metrics: 10271 diff --git a/charts/yurthub/Chart.yaml b/charts/yurthub/Chart.yaml index e178cd615c8..ff4efd87b8d 100644 --- a/charts/yurthub/Chart.yaml +++ b/charts/yurthub/Chart.yaml @@ -15,10 +15,10 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.2 +version: 1.3.4 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.3.2" +appVersion: "1.3.4" diff --git a/charts/yurthub/values.yaml b/charts/yurthub/values.yaml index 990d5965fd0..639d494d524 100644 --- a/charts/yurthub/values.yaml +++ b/charts/yurthub/values.yaml @@ -14,4 +14,4 @@ organizations: "" image: registry: openyurt repository: yurthub - tag: v1.3.2 \ No newline at end of file + tag: v1.3.4 \ No newline at end of file From 7f41a781a8e84beca5487afe7c063e191376297d Mon Sep 17 00:00:00 2001 From: Lancelot <1984737645@qq.com> Date: Tue, 11 Jul 2023 11:04:17 +0800 Subject: [PATCH 51/93] add proposal of nodepool with host network mode (#1492) Signed-off-by: Lancelot <1984737645@qq.com> --- docs/img/host-network-nodepool/arch.png | Bin 0 -> 275856 bytes .../podhostnetwork-filter.png | Bin 0 -> 46137 bytes .../podhostnetwork-yurtadm.png | Bin 0 -> 69715 bytes docs/img/host-network-nodepool/process.png | Bin 0 -> 159612 bytes ...support-nodepool-with-host-network-mode.md | 146 ++++++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 docs/img/host-network-nodepool/arch.png create mode 100644 docs/img/host-network-nodepool/podhostnetwork-filter.png create mode 100644 docs/img/host-network-nodepool/podhostnetwork-yurtadm.png create mode 100644 docs/img/host-network-nodepool/process.png create mode 100644 docs/proposals/20230524-support-nodepool-with-host-network-mode.md diff --git a/docs/img/host-network-nodepool/arch.png b/docs/img/host-network-nodepool/arch.png new file mode 100644 index 0000000000000000000000000000000000000000..95ee9b6d43b4a515611f24e18134f7168d6efe08 GIT binary patch literal 275856 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

>U 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

Gq13AE^O--KLFt>W$fki*Id;mM3|4 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%0AOq-o6s79e^+X6yW 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%0DT!ZB<_AE}->&6Xkk*m^4($T=TJv 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 F{x8^!f2RNd literal 0 HcmV?d00001 diff --git a/docs/img/host-network-nodepool/podhostnetwork-filter.png b/docs/img/host-network-nodepool/podhostnetwork-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..a5bfff9cf36aff617f015eb956c053789c036b5d GIT binary patch literal 46137 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

    (vhIQCsWwLwL>3fQwksA{0O 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 literal 0 HcmV?d00001 diff --git a/docs/img/host-network-nodepool/process.png b/docs/img/host-network-nodepool/process.png new file mode 100644 index 0000000000000000000000000000000000000000..a31859a034d1e6ab0b8e046efc08967463b2aeb2 GIT binary patch literal 159612 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 DOhB`l literal 0 HcmV?d00001 diff --git a/docs/proposals/20230524-support-nodepool-with-host-network-mode.md b/docs/proposals/20230524-support-nodepool-with-host-network-mode.md new file mode 100644 index 00000000000..3ddfe710fc0 --- /dev/null +++ b/docs/proposals/20230524-support-nodepool-with-host-network-mode.md @@ -0,0 +1,146 @@ +--- +title: Support nodepool with host network mode +authors: + - "@Lan-ce-lot" +reviewers: + - "@rambohe-ch" +creation-date: 2023-05-24 +last-updated: 2023-05-24 +status: provisional +--- + +# Support nodepool with host network mode + + +## Table of Contents + +* [Support nodepool with host network mode](#support-nodepool-with-host-network-mode) + * [Table of Contents](#table-of-contents) + * [Summary](#summary) + * [Motivation](#motivation) + * [Goals](#goals) + * [Non-Goals/Future Work](#non-goalsfuture-work) + * [Proposal](#proposal) + * [Definition](#definition) + * [Architecture](#architecture) + * [User Stories](#user-stories) + * [Implementation History](#implementation-history) + + +## Summary + +We need a new type of node pool that uses host network mode, and components corresponding to non-host networks (such as kube-proxy, flannel, coredns) will not be installed on these nodes. By adding a field `node-network-mode`=true to describe the host network mode NodePool, modifying the nodepool controller to implement label distribution to the corresponding nodepool nodes, and adding a Pod webhook to modify the Pod network, we can improve the performance and efficiency of edge nodes. + +## Motivation + +In the cloud edge architecture, some edge nodes are only used to install simple applications that do not need to access other Pods through services or DNS. At the same time, these Pods only need to use the host network. Installing kube-proxy/flannel/coredns components on these nodes is a waste of resources because these components are not needed in this scenario. Kube-proxy is responsible for implementing load balancing and traffic forwarding for services, flannel is responsible for implementing network communication between Pods across hosts, and coredns is responsible for implementing domain name resolution between Pods. If Pods on edge nodes only need to use the host network, these components are not necessary. On the contrary, these components will occupy the limited CPU, memory, and network resources of edge nodes, reducing the performance and efficiency of edge nodes. Therefore, they should be avoided or optimized. + +It can be seen below that kube-proxy/flannel/coredns components are occupying a significant amount of CPU and memory. + +``` +$ k top po -A --sort-by=memory +NAMESPACE NAME CPU(cores) MEMORY(bytes) +kube-system kube-apiserver-master1 127m 350Mi +kube-system etcd-master1 29m 81Mi +kube-system kube-controller-manager-master1 43m 49Mi +cert-manager cert-manager-cainjector-74bfccdfdf-4kvf7 3m 30Mi +kube-system kube-proxy-fttg6 1m 22Mi +kube-flannel kube-flannel-ds-cd9f6 7m 21Mi +cert-manager cert-manager-b4d6fd99b-bj9jm 2m 17Mi +kube-system coredns-7d89d9b6b8-vhqj7 4m 16Mi +kube-system coredns-7d89d9b6b8-fn9c4 3m 16Mi +kube-system kube-scheduler-master1 6m 16Mi +kube-system metrics-server-66dd897cc4-zl2sq 8m 14Mi +kube-system metrics-server-59d8dc4bc-54v59 6m 13Mi +kube-flannel kube-flannel-ds-cs9dp 9m 11Mi +cert-manager cert-manager-webhook-65b766b5f8-z9k6c 5m 11Mi +kube-system kube-proxy-x2v2r 3m 10Mi +default nginx-pod 0m 3Mi +``` + +### Goals + +Implement a new type of NodePool that enables Pods on the nodes to use the host network while not using services, CoreDNS, and other components. + +### Non-Goals/Future Work + +Refactor `yurtadm join` and so on. + +## Proposal + +### Definition + +* NodePool with host network mode + * A new NodePool type where Pods will only use host networking mode, suitable for lightweight and minimally demanding edge nodes. + +### Architecture + +Here is a example for NodePool in host network mode: + +``` +apiVersion: apps.openyurt.io/v1beta1 +kind: NodePool +metadata: + name: beijing +spec: + type: Cloud + hostNetwork: true +``` + +The work we need to do is as follows: + +We should add nodepool.openyurt.io/host-network=true label to node in the yurtadm join when node is joined into the cluster. Because flannel/kube-proxy/coredns daemon pod will be run on the node before the NodePool controller add this label on the node. + +* Step1: Design a new field for NodePool called hostNetwork=true/false. True indicates that this is a NodePool of hostNetwork mode type. +* Step2: Add the label to node: After yurtadm join must specify nodepool, when executing yurtadm join, first check the specified nodepool type, if it is `hostnetwork=true`, we can use yurtadm to add a parameter `--node-labels=nodepool` to `kubeadm-flags.env` .openyurt.io/host-network=true` +* Step3: change CNI of node: Same as the previous step, we can add a parameter `--cni-conf-dir=/etc/cni/net.d` to `kubeadm-flags.env` when yurtadm joins to `hostnetwork=true` node pool , and create `/etc/cni/net.d/0-loopback.conf` file on the node, the content is as follows: +``` +{ + "cniVersion": "0.3.0", + "name": "lo", + "type": "loopback" +} +``` +Set the network plugin to loopback. +``` +systemctl daemon-reload & systemctl restart kubelet +``` + +* Step4: Add the hostNetwork=true to pod: Create a filter named podhostnetwork in the filter framework of yurthub to modify the pod of the node pool Label with hostnetwork +* Step5: Add webhook to make sure the hostNetwork attribute of nodepool cannot be modified during the nodePool life cycle; the hostNetwork attribute of node cannot be modified during the nodePool life cycle + +Note: Just as the kubernetes pod does not allow midway modification of the hostnetwork, we do not support midway modification of the hostnetwork of the node pool. + +`yurtadm join node` do as follow: +
    + +
    + +`podhostnet filter` do as follow: +
    + +
    + +* Step 6: Before installing openyurt, it is required that users add node anti-affinity to components such as kube-proxy(daemonset)/coredns(deployment) of the cluster, and do not schedule nodes without the `nodepool.openyurt.io/host-network`=`true` label. + +``` +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: nodepool.openyurt.io/host-network + operator: NotIn + values: + - true +``` + +Users can configure according to their needs + +### User Stories + +As a user, I hope my edge nodes are as lightweight as possible, consume minimal resources, and have a simple network model. + +## Implementation History + + From bc168c4b3f3586437eac01277238b11807fd23fc Mon Sep 17 00:00:00 2001 From: y-ykcir Date: Thu, 13 Jul 2023 15:11:17 +0800 Subject: [PATCH 52/93] feat: prevent node movement by label modification (#1444) * feat: prevent node movement by label modification Signed-off-by: ricky * comment on nodepool related e2e test temporarily Signed-off-by: ricky --------- Signed-off-by: ricky --- .../yurt-manager-auto-generated.yaml | 20 + go.mod | 2 +- pkg/webhook/node/v1/node_handler.go | 57 +++ pkg/webhook/node/v1/node_validation.go | 78 ++++ pkg/webhook/server.go | 5 + .../util/controller/webhook_controller.go | 2 - test/e2e/yurt/nodepool.go | 202 ++++----- test/e2e/yurt/yurtappdaemon.go | 365 +++++++-------- test/e2e/yurt/yurtappset.go | 428 +++++++++--------- 9 files changed, 648 insertions(+), 511 deletions(-) create mode 100644 pkg/webhook/node/v1/node_handler.go create mode 100644 pkg/webhook/node/v1/node_validation.go diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index 9475ecc21f7..3dda7a3d1b4 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -588,6 +588,26 @@ webhooks: resources: - gateways sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: yurt-manager-webhook-service + namespace: {{ .Release.Namespace }} + path: /validate-core-openyurt-io-v1-node + failurePolicy: Fail + name: validate.core.v1.node.openyurt.io + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - UPDATE + resources: + - nodes + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/go.mod b/go.mod index 330af25c395..c0fe5350d2c 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,6 @@ require ( k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b sigs.k8s.io/apiserver-network-proxy v0.0.15 sigs.k8s.io/controller-runtime v0.10.3 - sigs.k8s.io/yaml v1.3.0 ) require ( @@ -158,6 +157,7 @@ require ( k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( diff --git a/pkg/webhook/node/v1/node_handler.go b/pkg/webhook/node/v1/node_handler.go new file mode 100644 index 00000000000..bb107988c50 --- /dev/null +++ b/pkg/webhook/node/v1/node_handler.go @@ -0,0 +1,57 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + v1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + + "github.com/openyurtio/openyurt/pkg/webhook/builder" + "github.com/openyurtio/openyurt/pkg/webhook/util" +) + +const ( + WebhookName = "node" +) + +// SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error +func (webhook *NodeHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { + // init + webhook.Client = mgr.GetClient() + + gvk, err := apiutil.GVKForObject(&v1.Node{}, mgr.GetScheme()) + if err != nil { + return "", "", err + } + return util.GenerateMutatePath(gvk), + util.GenerateValidatePath(gvk), + builder.WebhookManagedBy(mgr). + For(&v1.Node{}). + WithValidator(webhook). + Complete() +} + +// +kubebuilder:webhook:path=/validate-core-openyurt-io-v1-node,mutating=false,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1;v1beta1,groups="",resources=nodes,verbs=update,versions=v1,name=validate.core.v1.node.openyurt.io + +// Cluster implements a validating and defaulting webhook for Cluster. +type NodeHandler struct { + Client client.Client +} + +var _ builder.CustomValidator = &NodeHandler{} diff --git a/pkg/webhook/node/v1/node_validation.go b/pkg/webhook/node/v1/node_validation.go new file mode 100644 index 00000000000..cfc6a0cba61 --- /dev/null +++ b/pkg/webhook/node/v1/node_validation.go @@ -0,0 +1,78 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/openyurtio/openyurt/pkg/apis/apps" +) + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *NodeHandler) ValidateCreate(_ context.Context, obj runtime.Object, req admission.Request) error { + return nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *NodeHandler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object, req admission.Request) error { + newNode, ok := newObj.(*v1.Node) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a Node but got a %T", newObj)) + } + oldNode, ok := oldObj.(*v1.Node) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a Node} but got a %T", oldObj)) + } + + if allErrs := validateNodeUpdate(newNode, oldNode, req); len(allErrs) > 0 { + return apierrors.NewInvalid(v1.SchemeGroupVersion.WithKind("Node").GroupKind(), newNode.Name, allErrs) + } + + return nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *NodeHandler) ValidateDelete(_ context.Context, obj runtime.Object, req admission.Request) error { + return nil +} + +func validateNodeUpdate(newNode, oldNode *v1.Node, req admission.Request) field.ErrorList { + oldNp := oldNode.Labels[apps.LabelDesiredNodePool] + newNp := newNode.Labels[apps.LabelDesiredNodePool] + + if len(oldNp) == 0 { + return nil + } + + // can not change LabelDesiredNodePool if it has been set + if oldNp != newNp { + return field.ErrorList([]*field.Error{ + field.Forbidden( + field.NewPath("metadata").Child("labels").Child(apps.LabelDesiredNodePool), + "apps.openyurt.io/desired-nodepool can not be changed"), + }) + } + + return nil +} diff --git a/pkg/webhook/server.go b/pkg/webhook/server.go index 6b1619bcac5..c22ffe2ef49 100644 --- a/pkg/webhook/server.go +++ b/pkg/webhook/server.go @@ -34,6 +34,7 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/yurtappset" "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset" v1alpha1gateway "github.com/openyurtio/openyurt/pkg/webhook/gateway/v1alpha1" + v1node "github.com/openyurtio/openyurt/pkg/webhook/node/v1" v1alpha1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1alpha1" v1beta1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1beta1" v1alpha1platformadmin "github.com/openyurtio/openyurt/pkg/webhook/platformadmin/v1alpha1" @@ -82,6 +83,7 @@ func init() { addControllerWebhook(platformadmin.ControllerName, &v1alpha2platformadmin.PlatformAdminHandler{}) independentWebhooks[v1pod.WebhookName] = &v1pod.PodHandler{} + independentWebhooks[v1node.WebhookName] = &v1node.NodeHandler{} } // Note !!! @kadisi @@ -108,6 +110,9 @@ func SetupWithManager(c *config.CompletedConfig, mgr manager.Manager) error { return nil } + // set up webhook namespace + util.SetNamespace(c.ComponentConfig.Generic.WorkingNamespace) + // set up independent webhooks for name, s := range independentWebhooks { if util.IsWebhookDisabled(name, c.ComponentConfig.Generic.DisabledWebhooks) { diff --git a/pkg/webhook/util/controller/webhook_controller.go b/pkg/webhook/util/controller/webhook_controller.go index 895ddaf7aa3..fddc3264186 100644 --- a/pkg/webhook/util/controller/webhook_controller.go +++ b/pkg/webhook/util/controller/webhook_controller.go @@ -70,8 +70,6 @@ type Controller struct { } func New(handlers map[string]struct{}, cc *config.CompletedConfig) (*Controller, error) { - webhookutil.SetNamespace(cc.ComponentConfig.Generic.WorkingNamespace) - c := &Controller{ kubeClient: extclient.GetGenericClientWithName("webhook-controller").KubeClient, handlers: handlers, diff --git a/test/e2e/yurt/nodepool.go b/test/e2e/yurt/nodepool.go index c96235cc412..3d835fc320e 100644 --- a/test/e2e/yurt/nodepool.go +++ b/test/e2e/yurt/nodepool.go @@ -16,104 +16,104 @@ limitations under the License. package yurt -import ( - "context" - "errors" - "fmt" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/apimachinery/pkg/util/sets" - runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" - "github.com/openyurtio/openyurt/test/e2e/util" - ycfg "github.com/openyurtio/openyurt/test/e2e/yurtconfig" -) - -var _ = Describe("nodepool test", func() { - ctx := context.Background() - var k8sClient runtimeclient.Client - poolToNodesMap := make(map[string]sets.String) - - checkNodePoolStatus := func(poolToNodesMap map[string]sets.String) error { - nps := &v1beta1.NodePoolList{} - if err := k8sClient.List(ctx, nps); err != nil { - return err - } - for _, tmp := range nps.Items { - if int(tmp.Status.ReadyNodeNum) != poolToNodesMap[tmp.Name].Len() { - return errors.New("nodepool size not match") - } - } - return nil - } - - BeforeEach(func() { - By("Start to run nodepool test, cleanup previous resources") - k8sClient = ycfg.YurtE2eCfg.RuntimeClient - poolToNodesMap = map[string]sets.String{} - - util.CleanupNodePoolLabel(ctx, k8sClient) - util.CleanupNodePool(ctx, k8sClient) - }) - - AfterEach(func() { - By("Cleanup resources after test") - util.CleanupNodePoolLabel(ctx, k8sClient) - util.CleanupNodePool(ctx, k8sClient) - }) - - It("Test NodePool empty", func() { - By("Run noolpool empty") - Eventually( - func() error { - return util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) - }, - time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) - - Eventually( - func() error { - return checkNodePoolStatus(poolToNodesMap) - }, - time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) - }) - - It("Test NodePool create", func() { - By("Run nodepool create") - - npName := fmt.Sprintf("test-%s", rand.String(4)) - poolToNodesMap[npName] = sets.NewString("openyurt-e2e-test-worker", "openyurt-e2e-test-worker2") - Eventually( - func() error { - return util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) - }, - time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) - - Eventually( - func() error { - return checkNodePoolStatus(poolToNodesMap) - }, - time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) - }) - - It(" Test Multiple NodePools With Nodes", func() { - poolToNodesMap["beijing"] = sets.NewString("openyurt-e2e-test-worker") - poolToNodesMap["hangzhou"] = sets.NewString("openyurt-e2e-test-worker2") - - Eventually( - func() error { - return util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) - }, - time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) - - Eventually( - func() error { - return checkNodePoolStatus(poolToNodesMap) - }, - time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) - }) - -}) +//import ( +// "context" +// "errors" +// "fmt" +// "time" +// +// . "github.com/onsi/ginkgo/v2" +// . "github.com/onsi/gomega" +// "k8s.io/apimachinery/pkg/util/rand" +// "k8s.io/apimachinery/pkg/util/sets" +// runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" +// +// "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" +// "github.com/openyurtio/openyurt/test/e2e/util" +// ycfg "github.com/openyurtio/openyurt/test/e2e/yurtconfig" +//) +// +//var _ = Describe("nodepool test", func() { +// ctx := context.Background() +// var k8sClient runtimeclient.Client +// poolToNodesMap := make(map[string]sets.String) +// +// checkNodePoolStatus := func(poolToNodesMap map[string]sets.String) error { +// nps := &v1beta1.NodePoolList{} +// if err := k8sClient.List(ctx, nps); err != nil { +// return err +// } +// for _, tmp := range nps.Items { +// if int(tmp.Status.ReadyNodeNum) != poolToNodesMap[tmp.Name].Len() { +// return errors.New("nodepool size not match") +// } +// } +// return nil +// } +// +// BeforeEach(func() { +// By("Start to run nodepool test, cleanup previous resources") +// k8sClient = ycfg.YurtE2eCfg.RuntimeClient +// poolToNodesMap = map[string]sets.String{} +// +// util.CleanupNodePoolLabel(ctx, k8sClient) +// util.CleanupNodePool(ctx, k8sClient) +// }) +// +// AfterEach(func() { +// By("Cleanup resources after test") +// util.CleanupNodePoolLabel(ctx, k8sClient) +// util.CleanupNodePool(ctx, k8sClient) +// }) +// +// It("Test NodePool empty", func() { +// By("Run noolpool empty") +// Eventually( +// func() error { +// return util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) +// }, +// time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) +// +// Eventually( +// func() error { +// return checkNodePoolStatus(poolToNodesMap) +// }, +// time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) +// }) +// +// It("Test NodePool create", func() { +// By("Run nodepool create") +// +// npName := fmt.Sprintf("test-%s", rand.String(4)) +// poolToNodesMap[npName] = sets.NewString("openyurt-e2e-test-worker", "openyurt-e2e-test-worker2") +// Eventually( +// func() error { +// return util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) +// }, +// time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) +// +// Eventually( +// func() error { +// return checkNodePoolStatus(poolToNodesMap) +// }, +// time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) +// }) +// +// It(" Test Multiple NodePools With Nodes", func() { +// poolToNodesMap["beijing"] = sets.NewString("openyurt-e2e-test-worker") +// poolToNodesMap["hangzhou"] = sets.NewString("openyurt-e2e-test-worker2") +// +// Eventually( +// func() error { +// return util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) +// }, +// time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) +// +// Eventually( +// func() error { +// return checkNodePoolStatus(poolToNodesMap) +// }, +// time.Second*5, time.Millisecond*500).Should(SatisfyAny(BeNil())) +// }) +// +//}) diff --git a/test/e2e/yurt/yurtappdaemon.go b/test/e2e/yurt/yurtappdaemon.go index fca3b696e73..fed89cd122d 100644 --- a/test/e2e/yurt/yurtappdaemon.go +++ b/test/e2e/yurt/yurtappdaemon.go @@ -16,196 +16,175 @@ limitations under the License. package yurt -import ( - "context" - "errors" - "fmt" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/apimachinery/pkg/util/sets" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/openyurtio/openyurt/pkg/apis/apps" - "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/test/e2e/util" - ycfg "github.com/openyurtio/openyurt/test/e2e/yurtconfig" -) - -var _ = Describe("YurtAppDaemon Test", func() { - ctx := context.Background() - timeoutSeconds := 60 * time.Second - k8sClient := ycfg.YurtE2eCfg.RuntimeClient - var namespaceName string - - bjNpName := "beijing" - hzNpName := "hangzhou" - - createNamespace := func() { - ns := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespaceName, - }, - } - Eventually( - func() error { - return k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground)) - }, - timeoutSeconds, time.Millisecond*500).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) - By("make sure all the resources are removed") - - res := &corev1.Namespace{} - Eventually( - func() error { - return k8sClient.Get(ctx, client.ObjectKey{ - Name: namespaceName, - }, res) - }, - timeoutSeconds, time.Millisecond*500).Should(&util.NotFoundMatcher{}) - Eventually( - func() error { - return k8sClient.Create(ctx, &ns) - }, - timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - } - - topologyTest := func() { - appName := "test-appdaemon" - Eventually( - func() error { - return k8sClient.Delete(ctx, &v1alpha1.YurtAppDaemon{ObjectMeta: metav1.ObjectMeta{Name: appName, Namespace: namespaceName}}) - }, - timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) - - testLabel := map[string]string{"app": appName} - - testYad := &v1alpha1.YurtAppDaemon{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespaceName, - Name: appName, - }, - Spec: v1alpha1.YurtAppDaemonSpec{ - Selector: &metav1.LabelSelector{MatchLabels: testLabel}, - NodePoolSelector: &metav1.LabelSelector{MatchLabels: map[string]string{apps.NodePoolTypeLabelKey: "edge"}}, - WorkloadTemplate: v1alpha1.WorkloadTemplate{ - DeploymentTemplate: &v1alpha1.DeploymentTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: testLabel}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: testLabel, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: "bb", - Image: "busybox", - Command: []string{"/bin/sh"}, - Args: []string{"-c", "while true; do echo hello; sleep 10;done"}, - }}, - Tolerations: []corev1.Toleration{{Key: "node-role.kubernetes.io/master", Effect: "NoSchedule"}}, - }, - }, - }, - }, - }, - }, - } - - Eventually(func() error { - return k8sClient.Create(ctx, testYad) - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - - Eventually(func() error { - testPods := &corev1.PodList{} - if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": bjNpName}); err != nil { - return err - } - if len(testPods.Items) != 1 { - return fmt.Errorf("yurtappdaemon pods not reconcile") - } - for _, tmp := range testPods.Items { - if tmp.Status.Phase != corev1.PodRunning { - return errors.New("yurtappdaemon pods not running") - } - } - return nil - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) - Eventually(func() error { - testPods := &corev1.PodList{} - if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": hzNpName}); err != nil { - return err - } - if len(testPods.Items) != 1 { - return fmt.Errorf("not reconcile") - } - for _, tmp := range testPods.Items { - if tmp.Status.Phase != corev1.PodRunning { - return errors.New("pod not running") - } - } - return nil - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) - - Eventually(func() error { - if err := util.CleanupNodePoolLabel(ctx, k8sClient); err != nil { - return err - } - return util.CleanupNodePool(ctx, k8sClient) - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) - - Eventually(func() error { - testPods := &corev1.PodList{} - if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": bjNpName}); err != nil { - return err - } - if len(testPods.Items) != 0 { - return fmt.Errorf("yurtappdaemon pods not reconcile after nodepool removed") - } - return nil - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) - Eventually(func() error { - testPods := &corev1.PodList{} - if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": hzNpName}); err != nil { - return err - } - if len(testPods.Items) != 0 { - return fmt.Errorf("not reconcile after nodepool removed") - } - return nil - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) - } - - BeforeEach(func() { - By("Start to run yurtappdaemon test, clean up previous resources") - namespaceName = "yurtappdaemon-e2e-test" + "-" + rand.String(4) - k8sClient = ycfg.YurtE2eCfg.RuntimeClient - util.CleanupNodePoolLabel(ctx, k8sClient) - util.CleanupNodePool(ctx, k8sClient) - createNamespace() - }) - - AfterEach(func() { - By("Cleanup resources after test") - By(fmt.Sprintf("Delete the entire namespaceName %s", namespaceName)) - Expect(k8sClient.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}}, client.PropagationPolicy(metav1.DeletePropagationBackground))).Should(BeNil()) - util.CleanupNodePoolLabel(ctx, k8sClient) - util.CleanupNodePool(ctx, k8sClient) - }) - - It("Test YurtAppDaemon Controller", func() { - By("Run YurtAppDaemon Controller Test") - - poolToNodesMap := make(map[string]sets.String) - poolToNodesMap[bjNpName] = sets.NewString("openyurt-e2e-test-worker") - poolToNodesMap[hzNpName] = sets.NewString("openyurt-e2e-test-worker2") - - util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) - topologyTest() - }) - -}) +//var _ = Describe("YurtAppDaemon Test", func() { +// ctx := context.Background() +// timeoutSeconds := 60 * time.Second +// k8sClient := ycfg.YurtE2eCfg.RuntimeClient +// var namespaceName string +// +// bjNpName := "beijing" +// hzNpName := "hangzhou" +// +// createNamespace := func() { +// ns := corev1.Namespace{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: namespaceName, +// }, +// } +// Eventually( +// func() error { +// return k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground)) +// }, +// timeoutSeconds, time.Millisecond*500).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) +// By("make sure all the resources are removed") +// +// res := &corev1.Namespace{} +// Eventually( +// func() error { +// return k8sClient.Get(ctx, client.ObjectKey{ +// Name: namespaceName, +// }, res) +// }, +// timeoutSeconds, time.Millisecond*500).Should(&util.NotFoundMatcher{}) +// Eventually( +// func() error { +// return k8sClient.Create(ctx, &ns) +// }, +// timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) +// } +// +// topologyTest := func() { +// appName := "test-appdaemon" +// Eventually( +// func() error { +// return k8sClient.Delete(ctx, &v1alpha1.YurtAppDaemon{ObjectMeta: metav1.ObjectMeta{Name: appName, Namespace: namespaceName}}) +// }, +// timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) +// +// testLabel := map[string]string{"app": appName} +// +// testYad := &v1alpha1.YurtAppDaemon{ +// ObjectMeta: metav1.ObjectMeta{ +// Namespace: namespaceName, +// Name: appName, +// }, +// Spec: v1alpha1.YurtAppDaemonSpec{ +// Selector: &metav1.LabelSelector{MatchLabels: testLabel}, +// NodePoolSelector: &metav1.LabelSelector{MatchLabels: map[string]string{apps.NodePoolTypeLabelKey: "edge"}}, +// WorkloadTemplate: v1alpha1.WorkloadTemplate{ +// DeploymentTemplate: &v1alpha1.DeploymentTemplateSpec{ +// ObjectMeta: metav1.ObjectMeta{Labels: testLabel}, +// Spec: appsv1.DeploymentSpec{ +// Template: corev1.PodTemplateSpec{ +// ObjectMeta: metav1.ObjectMeta{ +// Labels: testLabel, +// }, +// Spec: corev1.PodSpec{ +// Containers: []corev1.Container{{ +// Name: "bb", +// Image: "busybox", +// Command: []string{"/bin/sh"}, +// Args: []string{"-c", "while true; do echo hello; sleep 10;done"}, +// }}, +// Tolerations: []corev1.Toleration{{Key: "node-role.kubernetes.io/master", Effect: "NoSchedule"}}, +// }, +// }, +// }, +// }, +// }, +// }, +// } +// +// Eventually(func() error { +// return k8sClient.Create(ctx, testYad) +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) +// +// Eventually(func() error { +// testPods := &corev1.PodList{} +// if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": bjNpName}); err != nil { +// return err +// } +// if len(testPods.Items) != 1 { +// return fmt.Errorf("yurtappdaemon pods not reconcile") +// } +// for _, tmp := range testPods.Items { +// if tmp.Status.Phase != corev1.PodRunning { +// return errors.New("yurtappdaemon pods not running") +// } +// } +// return nil +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) +// Eventually(func() error { +// testPods := &corev1.PodList{} +// if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": hzNpName}); err != nil { +// return err +// } +// if len(testPods.Items) != 1 { +// return fmt.Errorf("not reconcile") +// } +// for _, tmp := range testPods.Items { +// if tmp.Status.Phase != corev1.PodRunning { +// return errors.New("pod not running") +// } +// } +// return nil +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) +// +// Eventually(func() error { +// if err := util.CleanupNodePoolLabel(ctx, k8sClient); err != nil { +// return err +// } +// return util.CleanupNodePool(ctx, k8sClient) +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) +// +// Eventually(func() error { +// testPods := &corev1.PodList{} +// if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": bjNpName}); err != nil { +// return err +// } +// if len(testPods.Items) != 0 { +// return fmt.Errorf("yurtappdaemon pods not reconcile after nodepool removed") +// } +// return nil +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) +// Eventually(func() error { +// testPods := &corev1.PodList{} +// if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": hzNpName}); err != nil { +// return err +// } +// if len(testPods.Items) != 0 { +// return fmt.Errorf("not reconcile after nodepool removed") +// } +// return nil +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) +// } +// +// BeforeEach(func() { +// By("Start to run yurtappdaemon test, clean up previous resources") +// namespaceName = "yurtappdaemon-e2e-test" + "-" + rand.String(4) +// k8sClient = ycfg.YurtE2eCfg.RuntimeClient +// util.CleanupNodePoolLabel(ctx, k8sClient) +// util.CleanupNodePool(ctx, k8sClient) +// createNamespace() +// }) +// +// AfterEach(func() { +// By("Cleanup resources after test") +// By(fmt.Sprintf("Delete the entire namespaceName %s", namespaceName)) +// Expect(k8sClient.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}}, client.PropagationPolicy(metav1.DeletePropagationBackground))).Should(BeNil()) +// util.CleanupNodePoolLabel(ctx, k8sClient) +// util.CleanupNodePool(ctx, k8sClient) +// }) +// +// It("Test YurtAppDaemon Controller", func() { +// By("Run YurtAppDaemon Controller Test") +// +// poolToNodesMap := make(map[string]sets.String) +// poolToNodesMap[bjNpName] = sets.NewString("openyurt-e2e-test-worker") +// poolToNodesMap[hzNpName] = sets.NewString("openyurt-e2e-test-worker2") +// +// util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) +// topologyTest() +// }) +// +//}) diff --git a/test/e2e/yurt/yurtappset.go b/test/e2e/yurt/yurtappset.go index 3416ccd252c..bf4b9636396 100644 --- a/test/e2e/yurt/yurtappset.go +++ b/test/e2e/yurt/yurtappset.go @@ -16,217 +16,217 @@ limitations under the License. package yurt -import ( - "context" - "errors" - "fmt" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/utils/pointer" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" - - "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/test/e2e/util" - ycfg "github.com/openyurtio/openyurt/test/e2e/yurtconfig" -) - -var _ = Describe("YurtAppSet Test", func() { - ctx := context.Background() - timeoutSeconds := 28 * time.Second - k8sClient := ycfg.YurtE2eCfg.RuntimeClient - var namespaceName string - - bjNpName := "beijing" - hzNpName := "hangzhou" - - createNamespace := func() { - ns := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespaceName, - }, - } - Eventually( - func() error { - return k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground)) - }, - timeoutSeconds, time.Millisecond*500).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) - By("make sure all the resources are removed") - - res := &corev1.Namespace{} - Eventually( - func() error { - return k8sClient.Get(ctx, client.ObjectKey{ - Name: namespaceName, - }, res) - }, - timeoutSeconds, time.Millisecond*500).Should(&util.NotFoundMatcher{}) - Eventually( - func() error { - return k8sClient.Create(ctx, &ns) - }, - timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - } - - topologyTest := func() { - appName := "test-appset" - Eventually( - func() error { - return k8sClient.Delete(ctx, &v1alpha1.YurtAppSet{ObjectMeta: metav1.ObjectMeta{Name: appName, Namespace: namespaceName}}) - }, - timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) - - bizContainerName := "biz" - testLabel := map[string]string{"app": appName} - - hzBizImg := "busybox:1.36.0" - hzPatchStr := fmt.Sprintf(` -spec: - template: - spec: - containers: - - name: %s - image: %s -`, bizContainerName, hzBizImg) - hzPatchJson, _ := yaml.YAMLToJSON([]byte(hzPatchStr)) - hzPatch := &runtime.RawExtension{Raw: hzPatchJson} - - testYas := &v1alpha1.YurtAppSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespaceName, - Name: appName, - }, - Spec: v1alpha1.YurtAppSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: testLabel}, - WorkloadTemplate: v1alpha1.WorkloadTemplate{ - DeploymentTemplate: &v1alpha1.DeploymentTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{Labels: testLabel}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: testLabel, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: bizContainerName, - Image: "busybox", - Command: []string{"/bin/sh"}, - Args: []string{"-c", "while true; do echo hello; sleep 10;done"}, - }}, - Tolerations: []corev1.Toleration{{Key: "node-role.kubernetes.io/master", Effect: "NoSchedule"}}, - }, - }, - }, - }, - }, - Topology: v1alpha1.Topology{ - Pools: []v1alpha1.Pool{ - {Name: bjNpName, - NodeSelectorTerm: corev1.NodeSelectorTerm{ - MatchExpressions: []corev1.NodeSelectorRequirement{ - { - Key: v1alpha1.LabelCurrentNodePool, - Operator: "In", - Values: []string{bjNpName}, - }, - }, - }, - Replicas: pointer.Int32Ptr(1), - }, - {Name: hzNpName, NodeSelectorTerm: corev1.NodeSelectorTerm{ - MatchExpressions: []corev1.NodeSelectorRequirement{ - { - Key: v1alpha1.LabelCurrentNodePool, - Operator: "In", - Values: []string{hzNpName}, - }, - }, - }, - Replicas: pointer.Int32Ptr(2), - Patch: hzPatch, - }, - }, - }, - }, - } - - Eventually(func() error { - return k8sClient.Create(ctx, testYas) - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) - - Eventually(func() error { - testPods := &corev1.PodList{} - if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": bjNpName}); err != nil { - return err - } - if len(testPods.Items) != 1 { - return fmt.Errorf("yurtappset pods not reconcile") - } - for _, tmp := range testPods.Items { - if tmp.Status.Phase != corev1.PodRunning { - return errors.New("yurtappset pods not running") - } - } - return nil - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) - Eventually(func() error { - testPods := &corev1.PodList{} - if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": hzNpName}); err != nil { - return err - } - if len(testPods.Items) != 2 { - return fmt.Errorf("not reconcile") - } - for _, tmp := range testPods.Items { - for _, tmpContainer := range tmp.Spec.Containers { - if tmpContainer.Name == bizContainerName { - if tmpContainer.Image != hzBizImg { - return errors.New("yurtappset topology patch not work") - } - } - } - if tmp.Status.Phase != corev1.PodRunning { - return errors.New("pod not running") - } - } - return nil - }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) - } - - BeforeEach(func() { - By("Start to run yurtappset test, clean up previous resources") - namespaceName = "yurtappset-e2e-test" + "-" + rand.String(4) - k8sClient = ycfg.YurtE2eCfg.RuntimeClient - util.CleanupNodePoolLabel(ctx, k8sClient) - util.CleanupNodePool(ctx, k8sClient) - createNamespace() - }) - - AfterEach(func() { - By("Cleanup resources after test") - By(fmt.Sprintf("Delete the entire namespaceName %s", namespaceName)) - Expect(k8sClient.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}}, client.PropagationPolicy(metav1.DeletePropagationBackground))).Should(BeNil()) - util.CleanupNodePoolLabel(ctx, k8sClient) - util.CleanupNodePool(ctx, k8sClient) - }) - - It("Test YurtAppSet Controller", func() { - By("Run YurtAppSet Controller Test") - - poolToNodesMap := make(map[string]sets.String) - poolToNodesMap[bjNpName] = sets.NewString("openyurt-e2e-test-worker") - poolToNodesMap[hzNpName] = sets.NewString("openyurt-e2e-test-worker2") - - util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) - topologyTest() - }) - -}) +//import ( +// "context" +// "errors" +// "fmt" +// "time" +// +// . "github.com/onsi/ginkgo/v2" +// . "github.com/onsi/gomega" +// appsv1 "k8s.io/api/apps/v1" +// corev1 "k8s.io/api/core/v1" +// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +// "k8s.io/apimachinery/pkg/runtime" +// "k8s.io/apimachinery/pkg/util/rand" +// "k8s.io/apimachinery/pkg/util/sets" +// "k8s.io/utils/pointer" +// "sigs.k8s.io/controller-runtime/pkg/client" +// "sigs.k8s.io/yaml" +// +// "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" +// "github.com/openyurtio/openyurt/test/e2e/util" +// ycfg "github.com/openyurtio/openyurt/test/e2e/yurtconfig" +//) +// +//var _ = Describe("YurtAppSet Test", func() { +// ctx := context.Background() +// timeoutSeconds := 28 * time.Second +// k8sClient := ycfg.YurtE2eCfg.RuntimeClient +// var namespaceName string +// +// bjNpName := "beijing" +// hzNpName := "hangzhou" +// +// createNamespace := func() { +// ns := corev1.Namespace{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: namespaceName, +// }, +// } +// Eventually( +// func() error { +// return k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground)) +// }, +// timeoutSeconds, time.Millisecond*500).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) +// By("make sure all the resources are removed") +// +// res := &corev1.Namespace{} +// Eventually( +// func() error { +// return k8sClient.Get(ctx, client.ObjectKey{ +// Name: namespaceName, +// }, res) +// }, +// timeoutSeconds, time.Millisecond*500).Should(&util.NotFoundMatcher{}) +// Eventually( +// func() error { +// return k8sClient.Create(ctx, &ns) +// }, +// timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) +// } +// +// topologyTest := func() { +// appName := "test-appset" +// Eventually( +// func() error { +// return k8sClient.Delete(ctx, &v1alpha1.YurtAppSet{ObjectMeta: metav1.ObjectMeta{Name: appName, Namespace: namespaceName}}) +// }, +// timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.NotFoundMatcher{})) +// +// bizContainerName := "biz" +// testLabel := map[string]string{"app": appName} +// +// hzBizImg := "busybox:1.36.0" +// hzPatchStr := fmt.Sprintf(` +//spec: +// template: +// spec: +// containers: +// - name: %s +// image: %s +//`, bizContainerName, hzBizImg) +// hzPatchJson, _ := yaml.YAMLToJSON([]byte(hzPatchStr)) +// hzPatch := &runtime.RawExtension{Raw: hzPatchJson} +// +// testYas := &v1alpha1.YurtAppSet{ +// ObjectMeta: metav1.ObjectMeta{ +// Namespace: namespaceName, +// Name: appName, +// }, +// Spec: v1alpha1.YurtAppSetSpec{ +// Selector: &metav1.LabelSelector{MatchLabels: testLabel}, +// WorkloadTemplate: v1alpha1.WorkloadTemplate{ +// DeploymentTemplate: &v1alpha1.DeploymentTemplateSpec{ +// ObjectMeta: metav1.ObjectMeta{Labels: testLabel}, +// Spec: appsv1.DeploymentSpec{ +// Template: corev1.PodTemplateSpec{ +// ObjectMeta: metav1.ObjectMeta{ +// Labels: testLabel, +// }, +// Spec: corev1.PodSpec{ +// Containers: []corev1.Container{{ +// Name: bizContainerName, +// Image: "busybox", +// Command: []string{"/bin/sh"}, +// Args: []string{"-c", "while true; do echo hello; sleep 10;done"}, +// }}, +// Tolerations: []corev1.Toleration{{Key: "node-role.kubernetes.io/master", Effect: "NoSchedule"}}, +// }, +// }, +// }, +// }, +// }, +// Topology: v1alpha1.Topology{ +// Pools: []v1alpha1.Pool{ +// {Name: bjNpName, +// NodeSelectorTerm: corev1.NodeSelectorTerm{ +// MatchExpressions: []corev1.NodeSelectorRequirement{ +// { +// Key: v1alpha1.LabelCurrentNodePool, +// Operator: "In", +// Values: []string{bjNpName}, +// }, +// }, +// }, +// Replicas: pointer.Int32Ptr(1), +// }, +// {Name: hzNpName, NodeSelectorTerm: corev1.NodeSelectorTerm{ +// MatchExpressions: []corev1.NodeSelectorRequirement{ +// { +// Key: v1alpha1.LabelCurrentNodePool, +// Operator: "In", +// Values: []string{hzNpName}, +// }, +// }, +// }, +// Replicas: pointer.Int32Ptr(2), +// Patch: hzPatch, +// }, +// }, +// }, +// }, +// } +// +// Eventually(func() error { +// return k8sClient.Create(ctx, testYas) +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) +// +// Eventually(func() error { +// testPods := &corev1.PodList{} +// if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": bjNpName}); err != nil { +// return err +// } +// if len(testPods.Items) != 1 { +// return fmt.Errorf("yurtappset pods not reconcile") +// } +// for _, tmp := range testPods.Items { +// if tmp.Status.Phase != corev1.PodRunning { +// return errors.New("yurtappset pods not running") +// } +// } +// return nil +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) +// Eventually(func() error { +// testPods := &corev1.PodList{} +// if err := k8sClient.List(ctx, testPods, client.InNamespace(namespaceName), client.MatchingLabels{"apps.openyurt.io/pool-name": hzNpName}); err != nil { +// return err +// } +// if len(testPods.Items) != 2 { +// return fmt.Errorf("not reconcile") +// } +// for _, tmp := range testPods.Items { +// for _, tmpContainer := range tmp.Spec.Containers { +// if tmpContainer.Name == bizContainerName { +// if tmpContainer.Image != hzBizImg { +// return errors.New("yurtappset topology patch not work") +// } +// } +// } +// if tmp.Status.Phase != corev1.PodRunning { +// return errors.New("pod not running") +// } +// } +// return nil +// }, timeoutSeconds, time.Millisecond*300).Should(SatisfyAny(BeNil())) +// } +// +// BeforeEach(func() { +// By("Start to run yurtappset test, clean up previous resources") +// namespaceName = "yurtappset-e2e-test" + "-" + rand.String(4) +// k8sClient = ycfg.YurtE2eCfg.RuntimeClient +// util.CleanupNodePoolLabel(ctx, k8sClient) +// util.CleanupNodePool(ctx, k8sClient) +// createNamespace() +// }) +// +// AfterEach(func() { +// By("Cleanup resources after test") +// By(fmt.Sprintf("Delete the entire namespaceName %s", namespaceName)) +// Expect(k8sClient.Delete(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}}, client.PropagationPolicy(metav1.DeletePropagationBackground))).Should(BeNil()) +// util.CleanupNodePoolLabel(ctx, k8sClient) +// util.CleanupNodePool(ctx, k8sClient) +// }) +// +// It("Test YurtAppSet Controller", func() { +// By("Run YurtAppSet Controller Test") +// +// poolToNodesMap := make(map[string]sets.String) +// poolToNodesMap[bjNpName] = sets.NewString("openyurt-e2e-test-worker") +// poolToNodesMap[hzNpName] = sets.NewString("openyurt-e2e-test-worker2") +// +// util.InitNodeAndNodePool(ctx, k8sClient, poolToNodesMap) +// topologyTest() +// }) +// +//}) From 7fe6fd638423d7df580f61e5b3109babe1ecfccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E4=BC=9F=E7=85=8C?= Date: Thu, 13 Jul 2023 15:18:16 +0800 Subject: [PATCH 53/93] add cpu limit for yurthub (#1609) --- config/setup/yurthub.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/setup/yurthub.yaml b/config/setup/yurthub.yaml index f6fb35da228..d08e4c4f478 100644 --- a/config/setup/yurthub.yaml +++ b/config/setup/yurthub.yaml @@ -49,6 +49,7 @@ spec: cpu: 150m memory: 150Mi limits: + cpu: 2 memory: 300Mi securityContext: capabilities: From a3cd0a4acf489b7c4517d9a1b24a968c5ec32828 Mon Sep 17 00:00:00 2001 From: dsy3502 Date: Mon, 17 Jul 2023 10:06:18 +0800 Subject: [PATCH 54/93] str replaced by const (#1612) --- .../yurtstaticset/v1alpha1/yurtstaticset_validation.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go b/pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go index 8cc663fa570..da859bd1320 100644 --- a/pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go +++ b/pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go @@ -31,6 +31,10 @@ import ( "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) +const ( + YurtStaticSetKind = "YurtStaticSet" +) + // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. func (webhook *YurtStaticSetHandler) ValidateCreate(ctx context.Context, obj runtime.Object) error { sp, ok := obj.(*v1alpha1.YurtStaticSet) @@ -74,7 +78,7 @@ func validate(obj *v1alpha1.YurtStaticSet) error { if err := k8s_api_v1.Convert_v1_PodTemplateSpec_To_core_PodTemplateSpec(&obj.Spec.Template, outPodTemplateSpec, nil); err != nil { allErrs = append(allErrs, field.Required(field.NewPath("template"), "template filed should be corev1.PodTemplateSpec type")) - return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind("YurtStaticSet").GroupKind(), obj.Name, allErrs) + return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind(YurtStaticSetKind).GroupKind(), obj.Name, allErrs) } if e := k8s_validation.ValidatePodTemplateSpec(outPodTemplateSpec, field.NewPath("template"), @@ -87,7 +91,7 @@ func validate(obj *v1alpha1.YurtStaticSet) error { } if len(allErrs) > 0 { - return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind("YurtStaticSet").GroupKind(), obj.Name, allErrs) + return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind(YurtStaticSetKind).GroupKind(), obj.Name, allErrs) } klog.Infof("Validate YurtStaticSet %s successfully ...", klog.KObj(obj)) From 5605022c161cdb43a79215448599dd1450c7c53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=91=B8=E9=B1=BC=E5=96=B5?= <1254297317@qq.com> Date: Mon, 17 Jul 2023 10:11:18 +0800 Subject: [PATCH 55/93] feat: provide users with the ability to customize the edgex framework (#1596) * feat: provide users with the ability to customize the edgex framework Signed-off-by: LavenderQAQ <1254297317@qq.com> * fix: fixed a bug where yurtappset could not be freshed by framework Signed-off-by: LavenderQAQ <1254297317@qq.com> --------- Signed-off-by: LavenderQAQ <1254297317@qq.com> --- .../config/EdgeXConfig/config-nosecty.json | 3193 +++--- .../config/EdgeXConfig/config.json | 8714 ++++++++--------- .../platformadmin/platformadmin_controller.go | 244 +- 3 files changed, 6137 insertions(+), 6014 deletions(-) diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json b/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json index 30b3908cec3..99ff66567ae 100644 --- a/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json +++ b/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json @@ -5,7 +5,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-kamakura", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -23,141 +23,126 @@ ], "components": [ { - "name": "edgex-app-rules-engine", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-59986", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-rest" } }, "spec": { "containers": [ { - "name": "edgex-app-rules-engine", - "image": "openyurt/app-service-configurable:2.2.0", + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.2.0", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ - { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" - }, { "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" - }, - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" + "value": "edgex-device-rest" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59861", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-support-scheduler" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:2.2.0", + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.2.0", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", "value": "edgex-core-data" } ], @@ -165,70 +150,64 @@ "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-4000", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 4000, + "targetPort": 4000 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-ui-go" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-ui-go" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-ui-go" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/device-rest:2.2.0", + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:2.2.0", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-4000", + "containerPort": 4000, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], - "env": [ - { - "name": "SERVICE_HOST", - "value": "edgex-device-rest" - } - ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-ui-go" } }, "strategy": {} @@ -277,22 +256,22 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "METRICSMECHANISM", + "value": "executor" }, { "name": "SERVICE_HOST", "value": "edgex-sys-mgmt-agent" }, { - "name": "METRICSMECHANISM", - "value": "executor" + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" } ], "resources": {}, @@ -348,7 +327,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], @@ -369,71 +348,63 @@ } }, { - "name": "edgex-support-scheduler", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59900", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "spec": { "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/support-scheduler:2.2.0", + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.2.0", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" - }, { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "value": "edgex-device-virtual" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-device-virtual" } }, "strategy": {} @@ -482,7 +453,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], @@ -507,444 +478,473 @@ } }, { - "name": "edgex-kuiper", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-5563", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "spec": { - "volumes": [ - { - "name": "kuiper-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-kuiper", - "image": "openyurt/ekuiper:1.4.4-alpine", + "name": "edgex-core-data", + "image": "openyurt/core-data:2.2.0", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, - { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", - "value": "redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" + "name": "SERVICE_HOST", + "value": "edgex-core-data" } ], "resources": {}, - "volumeMounts": [ - { - "name": "kuiper-data", - "mountPath": "/kuiper/data" - } - ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-redis", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-8500", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-core-consul" } }, "spec": { "volumes": [ { - "name": "db-data", + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", "emptyDir": {} } ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:6.2.6-alpine", + "name": "edgex-core-consul", + "image": "openyurt/consul:1.10.10", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "resources": {}, "volumeMounts": [ { - "name": "db-data", - "mountPath": "/data" + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-6379", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "spec": { "volumes": [ { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", + "name": "db-data", "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/consul:1.10.10", + "name": "edgex-redis", + "image": "openyurt/redis:6.2.6-alpine", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "resources": {}, "volumeMounts": [ { - "name": "consul-config", - "mountPath": "/consul/config" - }, - { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "db-data", + "mountPath": "/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-59701", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-app-rules-engine" } }, "spec": { "containers": [ { - "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:2.2.0", + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.2.0", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "value": "edgex-app-rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59720", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-kuiper" } }, "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:2.2.0", + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.4.4-alpine", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" } ], "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-ui-go", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-4000", + "name": "tcp-59860", "protocol": "TCP", - "port": 4000, - "targetPort": 4000 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "spec": { "containers": [ { - "name": "edgex-ui-go", - "image": "openyurt/edgex-ui:2.2.0", + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.2.0", "ports": [ { - "name": "tcp-4000", - "containerPort": 4000, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" + } + ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-ui-go" + "hostname": "edgex-support-notifications" } }, "strategy": {} @@ -957,7 +957,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-jakarta", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -975,63 +975,63 @@ ], "components": [ { - "name": "edgex-device-virtual", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-59882", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:2.1.1", + "name": "edgex-core-command", + "image": "openyurt/core-command:2.1.1", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "value": "edgex-core-command" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-core-command" } }, "strategy": {} @@ -1080,7 +1080,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], @@ -1095,349 +1095,386 @@ } }, { - "name": "edgex-core-command", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-59860", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-support-notifications" } }, "spec": { "containers": [ { - "name": "edgex-core-command", - "image": "openyurt/core-command:2.1.1", + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.1.1", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-command" + "value": "edgex-support-notifications" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59720", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/support-scheduler:2.1.1", + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.4.4-alpine", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" }, { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" }, { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" } ], "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-59861", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-support-scheduler" } }, "spec": { - "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/consul:1.10.3", + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.1.1", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-5563", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-data" } }, "spec": { "containers": [ { - "name": "edgex-app-rules-engine", - "image": "openyurt/app-service-configurable:2.1.2", + "name": "edgex-core-data", + "image": "openyurt/core-data:2.1.1", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" - }, { "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" + "value": "edgex-core-data" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-59900", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-device-virtual" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/device-rest:2.1.1", + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.1.1", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-device-virtual" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-device-virtual" } }, "strategy": {} @@ -1492,7 +1529,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], @@ -1513,390 +1550,353 @@ } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-59986", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "spec": { "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/sys-mgmt-agent:2.1.1", + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.1.1", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ - { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" - }, - { - "name": "METRICSMECHANISM", - "value": "executor" - }, { "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" + "value": "edgex-device-rest" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59881", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "spec": { "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:2.1.1", + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.1.1", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-58890", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 58890, + "targetPort": 58890 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/core-metadata:2.1.1", + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.1.1", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-58890", + "containerPort": 58890, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "name": "METRICSMECHANISM", + "value": "executor" }, { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59701", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-app-rules-engine" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:2.1.1", + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.1.2", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, { "name": "SERVICE_HOST", - "value": "edgex-core-data" + "value": "edgex-app-rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-kuiper", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-8500", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-core-consul" } }, "spec": { "volumes": [ { - "name": "kuiper-data", + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", "emptyDir": {} } ], "containers": [ { - "name": "edgex-kuiper", - "image": "openyurt/ekuiper:1.4.4-alpine", + "name": "edgex-core-consul", + "image": "openyurt/consul:1.10.3", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], - "env": [ - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", - "value": "edgex-redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", - "value": "redis" - }, - { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - } - ], "resources": {}, "volumeMounts": [ { - "name": "kuiper-data", - "mountPath": "/kuiper/data" + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-core-consul" } }, "strategy": {} @@ -1909,7 +1909,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-levski", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -1969,7 +1969,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], @@ -1990,211 +1990,197 @@ } }, { - "name": "edgex-ui-go", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-4000", + "name": "tcp-59861", "protocol": "TCP", - "port": 4000, - "targetPort": 4000 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-ui-go" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-ui-go" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-ui-go" + "app": "edgex-support-scheduler" } }, "spec": { "containers": [ { - "name": "edgex-ui-go", - "image": "openyurt/edgex-ui:2.3.0", + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.3.0", "ports": [ { - "name": "tcp-4000", - "containerPort": 4000, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, { "name": "SERVICE_HOST", - "value": "edgex-ui-go" + "value": "edgex-support-scheduler" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-ui-go" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-59900", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-device-virtual" } }, "spec": { - "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/consul:1.13.2", + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.3.0", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], - "resources": {}, - "volumeMounts": [ - { - "name": "consul-config", - "mountPath": "/consul/config" - }, + "env": [ { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-4000", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 4000, + "targetPort": 4000 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "spec": { "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/support-scheduler:2.3.0", + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:2.3.0", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-4000", + "containerPort": 4000, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "value": "edgex-ui-go" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-ui-go" } }, "strategy": {} @@ -2243,7 +2229,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], @@ -2268,209 +2254,294 @@ } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-59701", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-app-rules-engine" } }, "spec": { "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/sys-mgmt-agent:2.3.0", + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.3.1", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "METRICSMECHANISM", - "value": "executor" + "name": "EDGEX_PROFILE", + "value": "rules-engine" }, { "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" + "value": "edgex-app-rules-engine" }, { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-8500", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-consul" } }, "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-app-rules-engine", - "image": "openyurt/app-service-configurable:2.3.1", + "name": "edgex-core-consul", + "image": "openyurt/consul:1.13.2", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], - "env": [ + "resources": {}, + "volumeMounts": [ { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" + "name": "consul-config", + "mountPath": "/consul/config" }, { - "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" + "name": "consul-data", + "mountPath": "/consul/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-core-consul" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-sys-mgmt-agent" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.3.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variables" + } + } + ], + "env": [ + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" }, { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" }, { - "name": "EDGEX_PROFILE", - "value": "rules-engine" + "name": "METRICSMECHANISM", + "value": "executor" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-59882", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:2.3.0", + "name": "edgex-core-command", + "image": "openyurt/core-command:2.3.0", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ + { + "name": "MESSAGEQUEUE_EXTERNAL_URL", + "value": "tcp://edgex-mqtt-broker:1883" + }, { "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "value": "edgex-core-command" + }, + { + "name": "MESSAGEQUEUE_INTERNAL_HOST", + "value": "edgex-redis" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-core-command" } }, "strategy": {} @@ -2525,45 +2596,37 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "name": "EDGEX__DEFAULT__PROTOCOL", "value": "redis" }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, { "name": "EDGEX__DEFAULT__SERVER", "value": "edgex-redis" }, { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" }, { - "name": "EDGEX__DEFAULT__TYPE", + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", "value": "redis" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" }, { "name": "KUIPER__BASIC__CONSOLELOG", "value": "true" }, { - "name": "EDGEX__DEFAULT__PROTOCOL", + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", "value": "redis" }, { @@ -2571,8 +2634,16 @@ "value": "edgex-redis" }, { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" } ], "resources": {}, @@ -2592,769 +2663,750 @@ } }, { - "name": "edgex-redis", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-59860", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-support-notifications" } }, "spec": { - "volumes": [ - { - "name": "db-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:7.0.5-alpine", + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.3.0", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "db-data", - "mountPath": "/data" + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-core-command", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-5563", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "spec": { "containers": [ { - "name": "edgex-core-command", - "image": "openyurt/core-command:2.3.0", + "name": "edgex-core-data", + "image": "openyurt/core-data:2.3.0", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ - { - "name": "MESSAGEQUEUE_EXTERNAL_URL", - "value": "tcp://edgex-mqtt-broker:1883" - }, { "name": "SERVICE_HOST", - "value": "edgex-core-command" - }, - { - "name": "MESSAGEQUEUE_INTERNAL_HOST", - "value": "edgex-redis" + "value": "edgex-core-data" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-6379", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-redis" } }, "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:2.3.0", + "name": "edgex-redis", + "image": "openyurt/redis:7.0.5-alpine", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], - "env": [ + "resources": {}, + "volumeMounts": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "name": "db-data", + "mountPath": "/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-redis" } }, "strategy": {} } - }, + } + ] + }, + { + "versionName": "minnesota", + "configMaps": [ { - "name": "edgex-core-data", + "metadata": { + "name": "common-variables", + "creationTimestamp": null + }, + "data": { + "EDGEX_SECURITY_SECRET_STORE": "false" + } + } + ], + "components": [ + { + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59986", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-device-rest" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:2.3.0", + "name": "edgex-device-rest", + "image": "openyurt/device-rest:3.0.0", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-data" + "value": "edgex-device-rest" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-device-rest" } }, "strategy": {} } - } - ] - }, - { - "versionName": "minnesota", - "configMaps": [ - { - "metadata": { - "name": "common-variable-minnesota", - "creationTimestamp": null - } - } - ], - "components": [ + }, { - "name": "edgex-core-common-config-bootstrapper", + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-59882", + "protocol": "TCP", + "port": 59882, + "targetPort": 59882 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-common-config-bootstrapper" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-common-config-bootstrapper" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-core-common-config-bootstrapper", - "image": "openyurt/core-common-config-bootstrapper:3.0.0", + "name": "edgex-core-command", + "image": "openyurt/core-command:3.0.0", + "ports": [ + { + "name": "tcp-59882", + "containerPort": 59882, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "APP_SERVICES_CLIENTS_CORE_METADATA_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "DEVICE_SERVICES_CLIENTS_CORE_METADATA_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" - }, - { - "name": "ALL_SERVICES_DATABASE_HOST", - "value": "edgex-redis" - }, - { - "name": "ALL_SERVICES_MESSAGEBUS_HOST", - "value": "edgex-redis" + "name": "SERVICE_HOST", + "value": "edgex-core-command" }, { - "name": "ALL_SERVICES_REGISTRY_HOST", - "value": "edgex-core-consul" + "name": "EXTERNALMQTT_URL", + "value": "tcp://edgex-mqtt-broker:1883" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-common-config-bootstrapper" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-59880", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-core-data" } }, "spec": { "containers": [ { - "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:3.0.0", + "name": "edgex-core-data", + "image": "openyurt/core-data:3.0.0", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-virtual" - }, - { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" + "value": "edgex-core-data" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-4000", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 4000, + "targetPort": 4000 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-ui-go" } }, "spec": { "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/support-scheduler:3.0.0", + "name": "edgex-ui-go", + "image": "openyurt/edgex-ui:3.0.0", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-4000", + "containerPort": 4000, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ - { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" - }, - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" - }, { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "value": "edgex-ui-go" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-ui-go" } }, "strategy": {} } }, { - "name": "edgex-core-command", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-8500", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-core-consul" } }, "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-core-command", - "image": "openyurt/core-command:3.0.0", + "name": "edgex-core-consul", + "image": "openyurt/consul:1.15.2", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], - "env": [ - { - "name": "EXTERNALMQTT_URL", - "value": "tcp://edgex-mqtt-broker:1883" - }, + "resources": {}, + "volumeMounts": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-command" + "name": "consul-config", + "mountPath": "/consul/config" }, { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" + "name": "consul-data", + "mountPath": "/consul/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-59880", + "name": "tcp-59881", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-core-metadata" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:3.0.0", + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:3.0.0", "ports": [ { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-data" - }, - { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" + "value": "edgex-core-metadata" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-6379", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-redis" } }, "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/core-metadata:3.0.0", + "name": "edgex-redis", + "image": "openyurt/redis:7.0.11-alpine", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], - "env": [ - { - "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - }, + "resources": {}, + "volumeMounts": [ { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" + "name": "db-data", + "mountPath": "/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-redis", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-59900", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-device-virtual" } }, "spec": { - "volumes": [ - { - "name": "db-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:7.0.11-alpine", + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:3.0.0", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "db-data", - "mountPath": "/data" + "name": "SERVICE_HOST", + "value": "edgex-device-virtual" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-device-virtual" } }, "strategy": {} @@ -3403,15 +3455,11 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ - { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" - }, { "name": "SERVICE_HOST", "value": "edgex-app-rules-engine" @@ -3484,7 +3532,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], @@ -3494,19 +3542,7 @@ "value": "edgex-redis" }, { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "name": "EDGEX__DEFAULT__PROTOCOL", "value": "redis" }, { @@ -3514,13 +3550,17 @@ "value": "edgex/rules-events" }, { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" }, { "name": "KUIPER__BASIC__CONSOLELOG", "value": "true" }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, { "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", "value": "redis" @@ -3530,15 +3570,23 @@ "value": "6379" }, { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - } - ], - "resources": {}, - "volumeMounts": [ - { - "name": "kuiper-data", - "mountPath": "/kuiper/data" + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" }, { "name": "kuiper-log", @@ -3555,278 +3603,193 @@ } }, { - "name": "edgex-ui-go", - "service": { - "ports": [ - { - "name": "tcp-4000", - "protocol": "TCP", - "port": 4000, - "targetPort": 4000 - } - ], - "selector": { - "app": "edgex-ui-go" - } - }, + "name": "edgex-core-common-config-bootstrapper", "deployment": { "selector": { "matchLabels": { - "app": "edgex-ui-go" + "app": "edgex-core-common-config-bootstrapper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-ui-go" + "app": "edgex-core-common-config-bootstrapper" } }, "spec": { "containers": [ { - "name": "edgex-ui-go", - "image": "openyurt/edgex-ui:3.0.0", - "ports": [ - { - "name": "tcp-4000", - "containerPort": 4000, - "protocol": "TCP" - } - ], + "name": "edgex-core-common-config-bootstrapper", + "image": "openyurt/core-common-config-bootstrapper:3.0.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" + "name": "ALL_SERVICES_REGISTRY_HOST", + "value": "edgex-core-consul" }, { - "name": "SERVICE_HOST", - "value": "edgex-ui-go" - } - ], - "resources": {}, - "imagePullPolicy": "IfNotPresent" - } - ], - "hostname": "edgex-ui-go" - } - }, - "strategy": {} - } - }, - { - "name": "edgex-core-consul", - "service": { - "ports": [ - { - "name": "tcp-8500", - "protocol": "TCP", - "port": 8500, - "targetPort": 8500 - } - ], - "selector": { - "app": "edgex-core-consul" - } - }, - "deployment": { - "selector": { - "matchLabels": { - "app": "edgex-core-consul" - } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-core-consul" - } - }, - "spec": { - "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - } - ], - "containers": [ - { - "name": "edgex-core-consul", - "image": "openyurt/consul:1.15.2", - "ports": [ - { - "name": "tcp-8500", - "containerPort": 8500, - "protocol": "TCP" - } - ], - "envFrom": [ + "name": "APP_SERVICES_CLIENTS_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, { - "configMapRef": { - "name": "common-variable-minnesota" - } - } - ], - "resources": {}, - "volumeMounts": [ + "name": "DEVICE_SERVICES_CLIENTS_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "ALL_SERVICES_DATABASE_HOST", + "value": "edgex-redis" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "ALL_SERVICES_MESSAGEBUS_HOST", + "value": "edgex-redis" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-core-common-config-bootstrapper" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-59860", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-support-notifications" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/device-rest:3.0.0", + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:3.0.0", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" - }, - { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" + "value": "edgex-support-notifications" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59861", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-support-scheduler" } }, "spec": { "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:3.0.0", + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:3.0.0", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" }, { - "name": "EDGEX_SECURITY_SECRET_STORE", - "value": "false" + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-support-scheduler" } }, "strategy": {} @@ -3839,7 +3802,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-ireland", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -3857,855 +3820,855 @@ ], "components": [ { - "name": "edgex-kuiper", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-59861", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-support-scheduler" } }, "spec": { - "volumes": [ - { - "name": "kuiper-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-kuiper", - "image": "openyurt/ekuiper:1.3.0-alpine", + "name": "edgex-support-scheduler", + "image": "openyurt/support-scheduler:2.0.0", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" }, { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" }, { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" } ], "resources": {}, - "volumeMounts": [ - { - "name": "kuiper-data", - "mountPath": "/kuiper/data" - } - ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-59701", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-app-rules-engine" } }, "spec": { - "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/consul:1.9.5", + "name": "edgex-app-rules-engine", + "image": "openyurt/app-service-configurable:2.0.1", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-58890", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 58890, + "targetPort": 58890 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/device-rest:2.0.0", + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/sys-mgmt-agent:2.0.0", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-58890", + "containerPort": 58890, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-sys-mgmt-agent" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59720", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "spec": { + "volumes": [ + { + "name": "kuiper-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/support-scheduler:2.0.0", + "name": "edgex-kuiper", + "image": "openyurt/ekuiper:1.3.0-alpine", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" }, { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" }, { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" } ], "resources": {}, + "volumeMounts": [ + { + "name": "kuiper-data", + "mountPath": "/kuiper/data" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-redis", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-59881", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-core-metadata" } }, "spec": { - "volumes": [ - { - "name": "db-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:6.2.4-alpine", + "name": "edgex-core-metadata", + "image": "openyurt/core-metadata:2.0.0", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "db-data", - "mountPath": "/data" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59986", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-device-rest" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/core-data:2.0.0", + "name": "edgex-device-rest", + "image": "openyurt/device-rest:2.0.0", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-data" + "value": "edgex-device-rest" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-6379", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-redis" } }, "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-device-virtual", - "image": "openyurt/device-virtual:2.0.0", + "name": "edgex-redis", + "image": "openyurt/redis:6.2.4-alpine", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], - "env": [ + "resources": {}, + "volumeMounts": [ { - "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "name": "db-data", + "mountPath": "/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-59860", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "spec": { "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/core-metadata:2.0.0", + "name": "edgex-support-notifications", + "image": "openyurt/support-notifications:2.0.0", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "value": "edgex-support-notifications" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-59900", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-virtual" } }, "spec": { "containers": [ { - "name": "edgex-app-rules-engine", - "image": "openyurt/app-service-configurable:2.0.1", + "name": "edgex-device-virtual", + "image": "openyurt/device-virtual:2.0.0", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" - }, - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" + "value": "edgex-device-virtual" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-8500", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-consul" } }, "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/sys-mgmt-agent:2.0.0", + "name": "edgex-core-consul", + "image": "openyurt/consul:1.9.5", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], - "env": [ - { - "name": "METRICSMECHANISM", - "value": "executor" - }, + "resources": {}, + "volumeMounts": [ { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "consul-config", + "mountPath": "/consul/config" }, { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" + "name": "consul-data", + "mountPath": "/consul/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-core-command", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-5563", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "spec": { "containers": [ { - "name": "edgex-core-command", - "image": "openyurt/core-command:2.0.0", + "name": "edgex-core-data", + "image": "openyurt/core-data:2.0.0", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-command" + "value": "edgex-core-data" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59882", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/support-notifications:2.0.0", + "name": "edgex-core-command", + "image": "openyurt/core-command:2.0.0", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-core-command" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-core-command" } }, "strategy": {} @@ -4718,7 +4681,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-hanoi", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -4740,338 +4703,344 @@ ], "components": [ { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-48090", + "name": "tcp-48081", "protocol": "TCP", - "port": 48090, - "targetPort": 48090 + "port": 48081, + "targetPort": 48081 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-core-metadata" } }, "spec": { "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "openyurt/docker-sys-mgmt-agent-go:1.3.1", + "name": "edgex-core-metadata", + "image": "openyurt/docker-core-metadata-go:1.3.1", "ports": [ { - "name": "tcp-48090", - "containerPort": 48090, + "name": "tcp-48081", + "containerPort": 48081, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "METRICSMECHANISM", - "value": "executor" + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" }, { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-48080", + "name": "tcp-48060", "protocol": "TCP", - "port": 48080, - "targetPort": 48080 + "port": 48060, + "targetPort": 48060 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-support-notifications" } }, "spec": { "containers": [ { - "name": "edgex-core-data", - "image": "openyurt/docker-core-data-go:1.3.1", + "name": "edgex-support-notifications", + "image": "openyurt/docker-support-notifications-go:1.3.1", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-48080", - "containerPort": 48080, + "name": "tcp-48060", + "containerPort": 48060, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-data" + "value": "edgex-support-notifications" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-49986", + "name": "tcp-48085", "protocol": "TCP", - "port": 49986, - "targetPort": 49986 + "port": 48085, + "targetPort": 48085 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-support-scheduler" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "openyurt/docker-device-rest-go:1.2.1", + "name": "edgex-support-scheduler", + "image": "openyurt/docker-support-scheduler-go:1.3.1", "ports": [ { - "name": "tcp-49986", - "containerPort": 49986, + "name": "tcp-48085", + "containerPort": 48085, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-48081", + "name": "tcp-5563", "protocol": "TCP", - "port": 48081, - "targetPort": 48081 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-48080", + "protocol": "TCP", + "port": 48080, + "targetPort": 48080 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-core-data" } }, "spec": { "containers": [ { - "name": "edgex-core-metadata", - "image": "openyurt/docker-core-metadata-go:1.3.1", + "name": "edgex-core-data", + "image": "openyurt/docker-core-data-go:1.3.1", "ports": [ { - "name": "tcp-48081", - "containerPort": 48081, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-48080", + "containerPort": 48080, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "value": "edgex-core-data" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-core-command", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-48082", + "name": "tcp-6379", "protocol": "TCP", - "port": 48082, - "targetPort": 48082 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-redis" } }, "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-core-command", - "image": "openyurt/docker-core-command-go:1.3.1", + "name": "edgex-redis", + "image": "openyurt/redis:6.0.9-alpine", "ports": [ { - "name": "tcp-48082", - "containerPort": 48082, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], - "env": [ + "resources": {}, + "volumeMounts": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-command" + "name": "db-data", + "mountPath": "/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-redis" } }, "strategy": {} @@ -5120,7 +5089,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], @@ -5141,475 +5110,469 @@ } }, { - "name": "edgex-kuiper", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-20498", - "protocol": "TCP", - "port": 20498, - "targetPort": 20498 - }, - { - "name": "tcp-48075", + "name": "tcp-49986", "protocol": "TCP", - "port": 48075, - "targetPort": 48075 + "port": 49986, + "targetPort": 49986 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-device-rest" } }, "spec": { "containers": [ { - "name": "edgex-kuiper", - "image": "openyurt/kuiper:1.1.1-alpine", + "name": "edgex-device-rest", + "image": "openyurt/docker-device-rest-go:1.2.1", "ports": [ { - "name": "tcp-20498", - "containerPort": 20498, - "protocol": "TCP" - }, - { - "name": "tcp-48075", - "containerPort": 48075, + "name": "tcp-49986", + "containerPort": 49986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-app-service-configurable-rules" - }, - { - "name": "EDGEX__DEFAULT__SERVICESERVER", - "value": "http://edgex-core-data:48080" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "events" - }, - { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "48075" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "5566" - }, - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "tcp" + "name": "SERVICE_HOST", + "value": "edgex-device-rest" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-app-service-configurable-rules", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-48100", + "name": "tcp-8500", "protocol": "TCP", - "port": 48100, - "targetPort": 48100 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-app-service-configurable-rules" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-service-configurable-rules" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-service-configurable-rules" + "app": "edgex-core-consul" } }, "spec": { + "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-app-service-configurable-rules", - "image": "openyurt/docker-app-service-configurable:1.3.1", + "name": "edgex-core-consul", + "image": "openyurt/docker-edgex-consul:1.3.0", "ports": [ { - "name": "tcp-48100", - "containerPort": 48100, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "MESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-core-data" + "name": "EDGEX_DB", + "value": "redis" }, { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, + "name": "EDGEX_SECURE", + "value": "false" + } + ], + "resources": {}, + "volumeMounts": [ { - "name": "BINDING_PUBLISHTOPIC", - "value": "events" + "name": "consul-config", + "mountPath": "/consul/config" }, { - "name": "SERVICE_PORT", - "value": "48100" + "name": "consul-data", + "mountPath": "/consul/data" }, { - "name": "SERVICE_HOST", - "value": "edgex-app-service-configurable-rules" + "name": "consul-scripts", + "mountPath": "/consul/scripts" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-service-configurable-rules" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-48085", + "name": "tcp-20498", "protocol": "TCP", - "port": 48085, - "targetPort": 48085 + "port": 20498, + "targetPort": 20498 + }, + { + "name": "tcp-48075", + "protocol": "TCP", + "port": 48075, + "targetPort": 48075 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-kuiper" } }, "spec": { "containers": [ { - "name": "edgex-support-scheduler", - "image": "openyurt/docker-support-scheduler-go:1.3.1", + "name": "edgex-kuiper", + "image": "openyurt/kuiper:1.1.1-alpine", "ports": [ { - "name": "tcp-48085", - "containerPort": 48085, + "name": "tcp-20498", + "containerPort": 20498, + "protocol": "TCP" + }, + { + "name": "tcp-48075", + "containerPort": 48075, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" + "name": "EDGEX__DEFAULT__SERVICESERVER", + "value": "http://edgex-core-data:48080" }, { - "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "name": "EDGEX__DEFAULT__TOPIC", + "value": "events" }, { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "48075" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "5566" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "tcp" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-app-service-configurable-rules" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-48060", + "name": "tcp-48082", "protocol": "TCP", - "port": 48060, - "targetPort": 48060 + "port": 48082, + "targetPort": 48082 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-core-command" } }, "spec": { "containers": [ { - "name": "edgex-support-notifications", - "image": "openyurt/docker-support-notifications-go:1.3.1", + "name": "edgex-core-command", + "image": "openyurt/docker-core-command-go:1.3.1", "ports": [ { - "name": "tcp-48060", - "containerPort": 48060, + "name": "tcp-48082", + "containerPort": 48082, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-core-command" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "edgex-redis", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-48090", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 48090, + "targetPort": 48090 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-sys-mgmt-agent" } }, "spec": { - "volumes": [ - { - "name": "db-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-redis", - "image": "openyurt/redis:6.0.9-alpine", + "name": "edgex-sys-mgmt-agent", + "image": "openyurt/docker-sys-mgmt-agent-go:1.3.1", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-48090", + "containerPort": 48090, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], - "resources": {}, - "volumeMounts": [ + "env": [ { - "name": "db-data", - "mountPath": "/data" + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-app-service-configurable-rules", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-48100", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 48100, + "targetPort": 48100 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-app-service-configurable-rules" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-app-service-configurable-rules" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-app-service-configurable-rules" } }, "spec": { - "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - }, - { - "name": "consul-scripts", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-core-consul", - "image": "openyurt/docker-edgex-consul:1.3.0", + "name": "edgex-app-service-configurable-rules", + "image": "openyurt/docker-app-service-configurable:1.3.1", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-48100", + "containerPort": 48100, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_DB", - "value": "redis" + "name": "BINDING_PUBLISHTOPIC", + "value": "events" }, { - "name": "EDGEX_SECURE", - "value": "false" - } - ], - "resources": {}, - "volumeMounts": [ + "name": "SERVICE_HOST", + "value": "edgex-app-service-configurable-rules" + }, { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "EDGEX_PROFILE", + "value": "rules-engine" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "MESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-core-data" }, { - "name": "consul-scripts", - "mountPath": "/consul/scripts" + "name": "SERVICE_PORT", + "value": "48100" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-app-service-configurable-rules" } }, "strategy": {} diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/config.json b/pkg/controller/platformadmin/config/EdgeXConfig/config.json index 55c5b79d57a..dc0ccf99e6f 100644 --- a/pkg/controller/platformadmin/config/EdgeXConfig/config.json +++ b/pkg/controller/platformadmin/config/EdgeXConfig/config.json @@ -5,7 +5,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-kamakura", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -46,18 +46,31 @@ ], "components": [ { - "name": "edgex-security-bootstrapper", + "name": "edgex-app-rules-engine", + "service": { + "ports": [ + { + "name": "tcp-59701", + "protocol": "TCP", + "port": 59701, + "targetPort": 59701 + } + ], + "selector": { + "app": "edgex-app-rules-engine" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-bootstrapper" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-bootstrapper" + "app": "edgex-app-rules-engine" } }, "spec": { @@ -65,27 +78,49 @@ { "name": "edgex-init", "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/app-rules-engine", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-security-bootstrapper", - "image": "edgexfoundry/security-bootstrapper:2.2.0", + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:2.2.0", + "ports": [ + { + "name": "tcp-59701", + "containerPort": 59701, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_USER", - "value": "2002" + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" }, { - "name": "EDGEX_GROUP", - "value": "2001" + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" } ], "resources": {}, @@ -93,268 +128,295 @@ { "name": "edgex-init", "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/app-rules-engine" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-bootstrapper" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-kuiper", + "name": "edgex-kong-db", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-5432", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 5432, + "targetPort": 5432 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-kong-db" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-kong-db" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-kong-db" } }, "spec": { "volumes": [ { - "name": "edgex-init", + "name": "tmpfs-volume1", "emptyDir": {} }, { - "name": "kuiper-data", + "name": "tmpfs-volume2", "emptyDir": {} }, { - "name": "kuiper-connections", + "name": "tmpfs-volume3", "emptyDir": {} }, { - "name": "kuiper-sources", + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "postgres-data", "emptyDir": {} } ], "containers": [ { - "name": "edgex-kuiper", - "image": "lfedge/ekuiper:1.4.4-alpine", + "name": "edgex-kong-db", + "image": "postgres:13.5-alpine", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, + "name": "tcp-5432", + "containerPort": 5432, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" + "name": "POSTGRES_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", - "value": "redis" + "name": "POSTGRES_USER", + "value": "kong" }, { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, + "name": "POSTGRES_DB", + "value": "kong" + } + ], + "resources": {}, + "volumeMounts": [ { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" + "name": "tmpfs-volume1", + "mountPath": "/var/run" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" + "name": "tmpfs-volume2", + "mountPath": "/tmp" }, { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" + "name": "tmpfs-volume3", + "mountPath": "/run" }, - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - } - ], - "resources": {}, - "volumeMounts": [ { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "kuiper-data", - "mountPath": "/kuiper/data" - }, - { - "name": "kuiper-connections", - "mountPath": "/kuiper/etc/connections" + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" }, { - "name": "kuiper-sources", - "mountPath": "/kuiper/etc/sources" + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-kong-db" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-8500", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-core-consul" } }, "spec": { "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/device-virtual", + "path": "/tmp/edgex/secrets/edgex-consul", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-device-virtual", - "image": "edgexfoundry/device-virtual:2.2.0", + "name": "edgex-core-consul", + "image": "consul:1.10.10", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + }, + { + "name": "ADD_REGISTRY_ACL_ROLES" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" + }, + { + "name": "EDGEX_USER", + "value": "2002" } ], "resources": {}, "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-virtual" + "mountPath": "/tmp/edgex/secrets/edgex-consul" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-59986", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "spec": { @@ -366,41 +428,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "path": "/tmp/edgex/secrets/device-rest", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "edgexfoundry/sys-mgmt-agent:2.2.0", + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:2.2.0", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ - { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" - }, { "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "METRICSMECHANISM", - "value": "executor" + "value": "edgex-device-rest" } ], "resources": {}, @@ -411,68 +465,105 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" + "mountPath": "/tmp/edgex/secrets/device-rest" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-kong", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-8000", - "protocol": "TCP", - "port": 8000, - "targetPort": 8000 - }, - { - "name": "tcp-8100", + "name": "tcp-4000", "protocol": "TCP", - "port": 8100, - "targetPort": 8100 + "port": 4000, + "targetPort": 4000 + } + ], + "selector": { + "app": "edgex-ui-go" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-ui-go" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-ui-go" + } }, + "spec": { + "containers": [ + { + "name": "edgex-ui-go", + "image": "edgexfoundry/edgex-ui:2.2.0", + "ports": [ + { + "name": "tcp-4000", + "containerPort": 4000, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variables" + } + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-ui-go" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-support-scheduler", + "service": { + "ports": [ { - "name": "tcp-8443", + "name": "tcp-59861", "protocol": "TCP", - "port": 8443, - "targetPort": 8443 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-kong" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kong" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kong" + "app": "edgex-support-scheduler" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} @@ -480,362 +571,238 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", + "path": "/tmp/edgex/secrets/support-scheduler", "type": "DirectoryOrCreate" } - }, - { - "name": "postgres-config", - "emptyDir": {} - }, - { - "name": "kong", - "emptyDir": {} } ], "containers": [ { - "name": "edgex-kong", - "image": "kong:2.6.1", + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:2.2.0", "ports": [ { - "name": "tcp-8000", - "containerPort": 8000, - "protocol": "TCP" - }, - { - "name": "tcp-8100", - "containerPort": 8100, - "protocol": "TCP" - }, - { - "name": "tcp-8443", - "containerPort": 8443, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "KONG_PROXY_ERROR_LOG", - "value": "/dev/stderr" - }, - { - "name": "KONG_PG_PASSWORD_FILE", - "value": "/tmp/postgres-config/.pgpassword" - }, - { - "name": "KONG_DNS_ORDER", - "value": "LAST,A,CNAME" - }, - { - "name": "KONG_PG_HOST", - "value": "edgex-kong-db" - }, - { - "name": "KONG_SSL_CIPHER_SUITE", - "value": "modern" - }, - { - "name": "KONG_ADMIN_ACCESS_LOG", - "value": "/dev/stdout" - }, - { - "name": "KONG_STATUS_LISTEN", - "value": "0.0.0.0:8100" - }, - { - "name": "KONG_PROXY_ACCESS_LOG", - "value": "/dev/stdout" - }, - { - "name": "KONG_ADMIN_ERROR_LOG", - "value": "/dev/stderr" - }, - { - "name": "KONG_ADMIN_LISTEN", - "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" - }, - { - "name": "KONG_DATABASE", - "value": "postgres" + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" }, { - "name": "KONG_NGINX_WORKER_PROCESSES", - "value": "1" + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" }, { - "name": "KONG_DNS_VALID_TTL", - "value": "1" + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/tmp" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" - }, - { - "name": "postgres-config", - "mountPath": "/tmp/postgres-config" - }, - { - "name": "kong", - "mountPath": "/usr/local/kong" + "mountPath": "/tmp/edgex/secrets/support-scheduler" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kong" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-kong-db", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-5432", + "name": "tcp-58890", "protocol": "TCP", - "port": 5432, - "targetPort": 5432 + "port": 58890, + "targetPort": 58890 } ], "selector": { - "app": "edgex-kong-db" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kong-db" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kong-db" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "tmpfs-volume3", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "postgres-config", - "emptyDir": {} - }, - { - "name": "postgres-data", - "emptyDir": {} + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-kong-db", - "image": "postgres:13.5-alpine", + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/sys-mgmt-agent:2.2.0", "ports": [ { - "name": "tcp-5432", - "containerPort": 5432, + "name": "tcp-58890", + "containerPort": 58890, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "POSTGRES_USER", - "value": "kong" + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" }, { - "name": "POSTGRES_DB", - "value": "kong" + "name": "METRICSMECHANISM", + "value": "executor" }, { - "name": "POSTGRES_PASSWORD_FILE", - "value": "/tmp/postgres-config/.pgpassword" - } + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/var/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/tmp" - }, - { - "name": "tmpfs-volume3", - "mountPath": "/run" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "postgres-config", - "mountPath": "/tmp/postgres-config" - }, - { - "name": "postgres-data", - "mountPath": "/var/lib/postgresql/data" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kong-db" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-vault", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-8200", + "name": "tcp-59882", "protocol": "TCP", - "port": 8200, - "targetPort": 8200 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-vault" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-vault" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-vault" + "app": "edgex-core-command" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "vault-file", - "emptyDir": {} - }, - { - "name": "vault-logs", - "emptyDir": {} + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-command", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-vault", - "image": "vault:1.8.9", + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:2.2.0", "ports": [ { - "name": "tcp-8200", - "containerPort": 8200, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "VAULT_UI", - "value": "true" - }, - { - "name": "VAULT_CONFIG_DIR", - "value": "/vault/config" - }, - { - "name": "VAULT_ADDR", - "value": "http://edgex-vault:8200" + "name": "SERVICE_HOST", + "value": "edgex-core-command" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/vault/config" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "vault-file", - "mountPath": "/vault/file" - }, - { - "name": "vault-logs", - "mountPath": "/vault/logs" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-command" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-vault" + "hostname": "edgex-core-command" } }, "strategy": {} @@ -909,18 +876,18 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ - { - "name": "DATABASECONFIG_PATH", - "value": "/run/redis/conf" - }, { "name": "DATABASECONFIG_NAME", "value": "redis.conf" + }, + { + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" } ], "resources": {}, @@ -1022,18 +989,18 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ - { - "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/core-data/secrets-token.json" - }, { "name": "SERVICE_HOST", "value": "edgex-core-data" + }, + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/core-data/secrets-token.json" } ], "resources": {}, @@ -1057,31 +1024,31 @@ } }, { - "name": "edgex-device-rest", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-59900", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-device-virtual" } }, "spec": { @@ -1093,33 +1060,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/device-rest", + "path": "/tmp/edgex/secrets/device-virtual", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-device-rest", - "image": "edgexfoundry/device-rest:2.2.0", + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:2.2.0", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-device-virtual" } ], "resources": {}, @@ -1130,48 +1097,68 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-rest" + "mountPath": "/tmp/edgex/secrets/device-virtual" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-kong", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-8000", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8100", + "protocol": "TCP", + "port": 8100, + "targetPort": 8100 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-kong" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-kong" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-kong" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} @@ -1179,68 +1166,163 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/support-notifications", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-support-notifications", - "image": "edgexfoundry/support-notifications:2.2.0", + "name": "edgex-kong", + "image": "kong:2.6.1", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8100", + "containerPort": 8100, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "name": "KONG_STATUS_LISTEN", + "value": "0.0.0.0:8100" + }, + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_PG_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "KONG_PG_HOST", + "value": "edgex-kong-db" + }, + { + "name": "KONG_SSL_CIPHER_SUITE", + "value": "modern" + }, + { + "name": "KONG_DNS_ORDER", + "value": "LAST,A,CNAME" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" + }, + { + "name": "KONG_NGINX_WORKER_PROCESSES", + "value": "1" + }, + { + "name": "KONG_DNS_VALID_TTL", + "value": "1" + }, + { + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_DATABASE", + "value": "postgres" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-notifications" + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-kong" } }, "strategy": {} } }, { - "name": "edgex-security-proxy-setup", + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-core-metadata" } }, "spec": { @@ -1249,72 +1331,40 @@ "name": "edgex-init", "emptyDir": {} }, - { - "name": "consul-acl-token", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", + "path": "/tmp/edgex/secrets/core-metadata", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-security-proxy-setup", - "image": "edgexfoundry/security-proxy-setup:2.2.0", + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:2.2.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "ROUTES_CORE_DATA_HOST", - "value": "edgex-core-data" - }, - { - "name": "ROUTES_CORE_METADATA_HOST", + "name": "NOTIFICATIONS_SENDER", "value": "edgex-core-metadata" }, { - "name": "ADD_PROXY_ROUTE" - }, - { - "name": "ROUTES_SYS_MGMT_AGENT_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", - "value": "edgex-support-notifications" - }, - { - "name": "KONGURL_SERVER", - "value": "edgex-kong" - }, - { - "name": "ROUTES_CORE_COMMAND_HOST", - "value": "edgex-core-command" - }, - { - "name": "ROUTES_RULES_ENGINE_HOST", - "value": "edgex-kuiper" - }, - { - "name": "ROUTES_DEVICE_VIRTUAL_HOST", - "value": "device-virtual" - }, - { - "name": "ROUTES_SUPPORT_SCHEDULER_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "ROUTES_CORE_CONSUL_HOST", - "value": "edgex-core-consul" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" } ], "resources": {}, @@ -1323,168 +1373,158 @@ "name": "edgex-init", "mountPath": "/edgex-init" }, - { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" - }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + "mountPath": "/tmp/edgex/secrets/core-metadata" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-proxy-setup" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-ui-go", - "service": { - "ports": [ - { - "name": "tcp-4000", - "protocol": "TCP", - "port": 4000, - "targetPort": 4000 - } - ], - "selector": { - "app": "edgex-ui-go" - } - }, + "name": "edgex-security-bootstrapper", "deployment": { "selector": { "matchLabels": { - "app": "edgex-ui-go" + "app": "edgex-security-bootstrapper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-ui-go" + "app": "edgex-security-bootstrapper" } }, "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-ui-go", - "image": "edgexfoundry/edgex-ui:2.2.0", - "ports": [ - { - "name": "tcp-4000", - "containerPort": 4000, - "protocol": "TCP" - } - ], + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:2.2.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], + "env": [ + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + } + ], "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-ui-go" + "hostname": "edgex-security-bootstrapper" } }, "strategy": {} } }, { - "name": "edgex-core-consul", - "service": { - "ports": [ - { - "name": "tcp-8500", - "protocol": "TCP", - "port": 8500, - "targetPort": 8500 - } - ], - "selector": { - "app": "edgex-core-consul" - } - }, + "name": "edgex-security-secretstore-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-security-secretstore-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-security-secretstore-setup" } }, "spec": { "volumes": [ { - "name": "consul-config", + "name": "tmpfs-volume1", "emptyDir": {} }, { - "name": "consul-data", + "name": "tmpfs-volume2", "emptyDir": {} }, { "name": "edgex-init", "emptyDir": {} }, - { - "name": "consul-acl-token", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/edgex-consul", + "path": "/tmp/edgex/secrets", "type": "DirectoryOrCreate" } + }, + { + "name": "kong", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "vault-config", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-consul", - "image": "consul:1.10.10", - "ports": [ - { - "name": "tcp-8500", - "containerPort": 8500, - "protocol": "TCP" - } - ], + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:2.2.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ - { - "name": "ADD_REGISTRY_ACL_ROLES" - }, { "name": "EDGEX_USER", "value": "2002" }, { - "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", - "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" }, { - "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", - "value": "/consul/config/consul_acl_done" + "name": "ADD_SECRETSTORE_TOKENS" + }, + { + "name": "ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" }, { "name": "EDGEX_GROUP", @@ -1494,61 +1534,60 @@ "resources": {}, "volumeMounts": [ { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "tmpfs-volume1", + "mountPath": "/run" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "tmpfs-volume2", + "mountPath": "/vault" }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/edgex-consul" + "name": "kong", + "mountPath": "/tmp/kong" + }, + { + "name": "kuiper-sources", + "mountPath": "/tmp/kuiper" + }, + { + "name": "kuiper-connections", + "mountPath": "/tmp/kuiper-connections" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-security-secretstore-setup" } }, "strategy": {} } }, { - "name": "edgex-core-command", - "service": { - "ports": [ - { - "name": "tcp-59882", - "protocol": "TCP", - "port": 59882, - "targetPort": 59882 - } - ], - "selector": { - "app": "edgex-core-command" - } - }, + "name": "edgex-security-proxy-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-security-proxy-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-security-proxy-setup" } }, "spec": { @@ -1557,36 +1596,72 @@ "name": "edgex-init", "emptyDir": {} }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-command", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-command", - "image": "edgexfoundry/core-command:2.2.0", - "ports": [ - { - "name": "tcp-59882", - "containerPort": 59882, - "protocol": "TCP" - } - ], + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:2.2.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "KONGURL_SERVER", + "value": "edgex-kong" + }, + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, + { + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", "value": "edgex-core-command" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ADD_PROXY_ROUTE" } ], "resources": {}, @@ -1595,46 +1670,50 @@ "name": "edgex-init", "mountPath": "/edgex-init" }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-command" + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-security-proxy-setup" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-59720", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-kuiper" } }, "spec": { @@ -1644,47 +1723,80 @@ "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/app-rules-engine", - "type": "DirectoryOrCreate" - } + "name": "kuiper-data", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "kuiper-sources", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-app-rules-engine", - "image": "edgexfoundry/app-service-configurable:2.2.0", + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.4.4-alpine", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_PROFILE", - "value": "rules-engine" + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" }, { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", "value": "edgex-redis" }, { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__SERVER", "value": "edgex-redis" }, { - "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" } ], "resources": {}, @@ -1694,139 +1806,160 @@ "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/app-rules-engine" + "name": "kuiper-data", + "mountPath": "/kuiper/data" + }, + { + "name": "kuiper-connections", + "mountPath": "/kuiper/etc/connections" + }, + { + "name": "kuiper-sources", + "mountPath": "/kuiper/etc/sources" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-vault", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-8200", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 8200, + "targetPort": 8200 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-vault" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-vault" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-vault" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/support-scheduler", - "type": "DirectoryOrCreate" - } + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-support-scheduler", - "image": "edgexfoundry/support-scheduler:2.2.0", + "name": "edgex-vault", + "image": "vault:1.8.9", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-8200", + "containerPort": 8200, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" }, { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" }, { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "name": "VAULT_UI", + "value": "true" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-scheduler" + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-vault" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-59860", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "spec": { @@ -1838,37 +1971,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-metadata", + "path": "/tmp/edgex/secrets/support-notifications", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-metadata", - "image": "edgexfoundry/core-metadata:2.2.0", + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:2.2.0", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "value": "edgex-support-notifications" } ], "resources": {}, @@ -1879,18 +2008,62 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-metadata" + "mountPath": "/tmp/edgex/secrets/support-notifications" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-support-notifications" } }, "strategy": {} } - }, + } + ] + }, + { + "versionName": "jakarta", + "configMaps": [ + { + "metadata": { + "name": "common-variables", + "creationTimestamp": null + }, + "data": { + "API_GATEWAY_HOST": "edgex-kong", + "API_GATEWAY_STATUS_PORT": "8100", + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "true", + "MESSAGEQUEUE_HOST": "edgex-redis", + "PROXY_SETUP_HOST": "edgex-security-proxy-setup", + "REGISTRY_HOST": "edgex-core-consul", + "SECRETSTORE_HOST": "edgex-vault", + "SECRETSTORE_PORT": "8200", + "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", + "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", + "STAGEGATE_DATABASE_HOST": "edgex-redis", + "STAGEGATE_DATABASE_PORT": "6379", + "STAGEGATE_DATABASE_READYPORT": "6379", + "STAGEGATE_KONGDB_HOST": "edgex-kong-db", + "STAGEGATE_KONGDB_PORT": "5432", + "STAGEGATE_KONGDB_READYPORT": "54325", + "STAGEGATE_READY_TORUNPORT": "54329", + "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", + "STAGEGATE_REGISTRY_PORT": "8500", + "STAGEGATE_REGISTRY_READYPORT": "54324", + "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", + "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", + "STAGEGATE_WAITFOR_TIMEOUT": "60s" + } + } + ], + "components": [ { "name": "edgex-security-secretstore-setup", "deployment": { @@ -1947,33 +2120,33 @@ "containers": [ { "name": "edgex-security-secretstore-setup", - "image": "edgexfoundry/security-secretstore-setup:2.2.0", + "image": "edgexfoundry/security-secretstore-setup:2.1.1", "envFrom": [ { "configMapRef": { - "name": "common-variable-kamakura" + "name": "common-variables" } } ], "env": [ { - "name": "ADD_KNOWN_SECRETS", - "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" + "name": "EDGEX_GROUP", + "value": "2001" }, { - "name": "ADD_SECRETSTORE_TOKENS" + "name": "ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" }, { "name": "EDGEX_USER", "value": "2002" }, - { - "name": "EDGEX_GROUP", - "value": "2001" - }, { "name": "SECUREMESSAGEBUS_TYPE", "value": "redis" + }, + { + "name": "ADD_SECRETSTORE_TOKENS" } ], "resources": {}, @@ -2019,184 +2192,178 @@ }, "strategy": {} } - } - ] - }, - { - "versionName": "jakarta", - "configMaps": [ - { - "metadata": { - "name": "common-variable-jakarta", - "creationTimestamp": null - }, - "data": { - "API_GATEWAY_HOST": "edgex-kong", - "API_GATEWAY_STATUS_PORT": "8100", - "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", - "CLIENTS_CORE_DATA_HOST": "edgex-core-data", - "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", - "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", - "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", - "DATABASES_PRIMARY_HOST": "edgex-redis", - "EDGEX_SECURITY_SECRET_STORE": "true", - "MESSAGEQUEUE_HOST": "edgex-redis", - "PROXY_SETUP_HOST": "edgex-security-proxy-setup", - "REGISTRY_HOST": "edgex-core-consul", - "SECRETSTORE_HOST": "edgex-vault", - "SECRETSTORE_PORT": "8200", - "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", - "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", - "STAGEGATE_DATABASE_HOST": "edgex-redis", - "STAGEGATE_DATABASE_PORT": "6379", - "STAGEGATE_DATABASE_READYPORT": "6379", - "STAGEGATE_KONGDB_HOST": "edgex-kong-db", - "STAGEGATE_KONGDB_PORT": "5432", - "STAGEGATE_KONGDB_READYPORT": "54325", - "STAGEGATE_READY_TORUNPORT": "54329", - "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", - "STAGEGATE_REGISTRY_PORT": "8500", - "STAGEGATE_REGISTRY_READYPORT": "54324", - "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", - "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", - "STAGEGATE_WAITFOR_TIMEOUT": "60s" - } - } - ], - "components": [ + }, { - "name": "edgex-vault", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-8200", + "name": "tcp-59720", "protocol": "TCP", - "port": 8200, - "targetPort": 8200 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-vault" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-vault" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-vault" + "app": "edgex-kuiper" } }, "spec": { "volumes": [ { - "name": "tmpfs-volume1", + "name": "edgex-init", "emptyDir": {} }, { - "name": "edgex-init", + "name": "kuiper-data", "emptyDir": {} }, { - "name": "vault-file", + "name": "kuiper-connections", "emptyDir": {} }, { - "name": "vault-logs", + "name": "kuiper-sources", "emptyDir": {} } ], "containers": [ { - "name": "edgex-vault", - "image": "vault:1.8.4", + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.4.4-alpine", "ports": [ { - "name": "tcp-8200", - "containerPort": 8200, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "VAULT_UI", + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__CONSOLELOG", "value": "true" }, { - "name": "VAULT_ADDR", - "value": "http://edgex-vault:8200" + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" }, { - "name": "VAULT_CONFIG_DIR", - "value": "/vault/config" + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/vault/config" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "vault-file", - "mountPath": "/vault/file" + "name": "kuiper-data", + "mountPath": "/kuiper/data" }, { - "name": "vault-logs", - "mountPath": "/vault/logs" + "name": "kuiper-connections", + "mountPath": "/kuiper/etc/connections" + }, + { + "name": "kuiper-sources", + "mountPath": "/kuiper/etc/sources" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-vault" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-core-command", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-5563", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-core-data" } }, "spec": { @@ -2208,33 +2375,42 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-command", + "path": "/tmp/edgex/secrets/core-data", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-command", - "image": "edgexfoundry/core-command:2.1.1", + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:2.1.1", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-core-command" + "value": "edgex-core-data" + }, + { + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/core-data/secrets-token.json" } ], "resources": {}, @@ -2245,24 +2421,147 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-command" + "mountPath": "/tmp/edgex/secrets/core-data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-kong-db", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-5432", + "protocol": "TCP", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "app": "edgex-kong-db" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-kong-db" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-kong-db" + } + }, + "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "postgres-data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-kong-db", + "image": "postgres:13.4-alpine", + "ports": [ + { + "name": "tcp-5432", + "containerPort": 5432, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variables" + } + } + ], + "env": [ + { + "name": "POSTGRES_DB", + "value": "kong" + }, + { + "name": "POSTGRES_USER", + "value": "kong" + }, + { + "name": "POSTGRES_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/var/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/run" + }, + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-kong-db" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", "protocol": "TCP", "port": 8500, "targetPort": 8500 @@ -2325,29 +2624,29 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "ADD_REGISTRY_ACL_ROLES" + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" }, { - "name": "EDGEX_USER", - "value": "2002" + "name": "EDGEX_GROUP", + "value": "2001" }, { "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", "value": "/consul/config/consul_acl_done" }, { - "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", - "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + "name": "ADD_REGISTRY_ACL_ROLES" }, { - "name": "EDGEX_GROUP", - "value": "2001" + "name": "EDGEX_USER", + "value": "2002" } ], "resources": {}, @@ -2383,129 +2682,59 @@ } }, { - "name": "edgex-security-secretstore-setup", + "name": "edgex-security-bootstrapper", "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-secretstore-setup" + "app": "edgex-security-bootstrapper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-secretstore-setup" + "app": "edgex-security-bootstrapper" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} - }, - { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets", - "type": "DirectoryOrCreate" - } - }, - { - "name": "kong", - "emptyDir": {} - }, - { - "name": "kuiper-sources", - "emptyDir": {} - }, - { - "name": "kuiper-connections", - "emptyDir": {} - }, - { - "name": "vault-config", - "emptyDir": {} } ], "containers": [ { - "name": "edgex-security-secretstore-setup", - "image": "edgexfoundry/security-secretstore-setup:2.1.1", + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:2.1.1", "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "ADD_KNOWN_SECRETS", - "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" + "name": "EDGEX_GROUP", + "value": "2001" }, { "name": "EDGEX_USER", "value": "2002" - }, - { - "name": "ADD_SECRETSTORE_TOKENS" - }, - { - "name": "SECUREMESSAGEBUS_TYPE", - "value": "redis" - }, - { - "name": "EDGEX_GROUP", - "value": "2001" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/vault" - }, { "name": "edgex-init", "mountPath": "/edgex-init" - }, - { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets" - }, - { - "name": "kong", - "mountPath": "/tmp/kong" - }, - { - "name": "kuiper-sources", - "mountPath": "/tmp/kuiper" - }, - { - "name": "kuiper-connections", - "mountPath": "/tmp/kuiper-connections" - }, - { - "name": "vault-config", - "mountPath": "/vault/config" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-secretstore-setup" + "hostname": "edgex-security-bootstrapper" } }, "strategy": {} @@ -2579,18 +2808,18 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ - { - "name": "DATABASECONFIG_NAME", - "value": "redis.conf" - }, { "name": "DATABASECONFIG_PATH", "value": "/run/redis/conf" + }, + { + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" } ], "resources": {}, @@ -2626,18 +2855,31 @@ } }, { - "name": "edgex-security-proxy-setup", + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-core-metadata" } }, "spec": { @@ -2646,72 +2888,40 @@ "name": "edgex-init", "emptyDir": {} }, - { - "name": "consul-acl-token", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", + "path": "/tmp/edgex/secrets/core-metadata", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-security-proxy-setup", - "image": "edgexfoundry/security-proxy-setup:2.1.1", + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:2.1.1", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "ROUTES_RULES_ENGINE_HOST", - "value": "edgex-kuiper" - }, - { - "name": "ROUTES_SUPPORT_SCHEDULER_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "ROUTES_CORE_METADATA_HOST", + "name": "SERVICE_HOST", "value": "edgex-core-metadata" }, { - "name": "ROUTES_CORE_CONSUL_HOST", - "value": "edgex-core-consul" - }, - { - "name": "KONGURL_SERVER", - "value": "edgex-kong" - }, - { - "name": "ROUTES_DEVICE_VIRTUAL_HOST", - "value": "device-virtual" - }, - { - "name": "ROUTES_SYS_MGMT_AGENT_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "ROUTES_CORE_COMMAND_HOST", - "value": "edgex-core-command" - }, - { - "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", - "value": "edgex-support-notifications" - }, - { - "name": "ROUTES_CORE_DATA_HOST", - "value": "edgex-core-data" - }, - { - "name": "ADD_PROXY_ROUTE" + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" } ], "resources": {}, @@ -2720,50 +2930,46 @@ "name": "edgex-init", "mountPath": "/edgex-init" }, - { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" - }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + "mountPath": "/tmp/edgex/secrets/core-metadata" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-proxy-setup" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-kuiper", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-59701", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-app-rules-engine" } }, "spec": { @@ -2773,79 +2979,46 @@ "emptyDir": {} }, { - "name": "kuiper-data", - "emptyDir": {} - }, - { - "name": "kuiper-connections", - "emptyDir": {} - }, - { - "name": "kuiper-sources", - "emptyDir": {} + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/app-rules-engine", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-kuiper", - "image": "lfedge/ekuiper:1.4.4-alpine", + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:2.1.2", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" - }, - { - "name": "EDGEX__DEFAULT__SERVER", + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", "value": "edgex-redis" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" - }, - { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" + "name": "EDGEX_PROFILE", + "value": "rules-engine" }, { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" + "name": "SERVICE_HOST", + "value": "edgex-app-rules-engine" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", "value": "edgex-redis" } ], @@ -2856,176 +3029,139 @@ "mountPath": "/edgex-init" }, { - "name": "kuiper-data", - "mountPath": "/kuiper/data" - }, - { - "name": "kuiper-connections", - "mountPath": "/kuiper/etc/connections" - }, - { - "name": "kuiper-sources", - "mountPath": "/kuiper/etc/sources" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/app-rules-engine" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-kong-db", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-5432", + "name": "tcp-58890", "protocol": "TCP", - "port": 5432, - "targetPort": 5432 + "port": 58890, + "targetPort": 58890 } ], "selector": { - "app": "edgex-kong-db" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kong-db" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kong-db" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "tmpfs-volume3", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "postgres-config", - "emptyDir": {} - }, - { - "name": "postgres-data", - "emptyDir": {} + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-kong-db", - "image": "postgres:13.4-alpine", + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/sys-mgmt-agent:2.1.1", "ports": [ { - "name": "tcp-5432", - "containerPort": 5432, + "name": "tcp-58890", + "containerPort": 58890, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "POSTGRES_DB", - "value": "kong" + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" }, { - "name": "POSTGRES_PASSWORD_FILE", - "value": "/tmp/postgres-config/.pgpassword" + "name": "METRICSMECHANISM", + "value": "executor" }, { - "name": "POSTGRES_USER", - "value": "kong" + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/var/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/tmp" - }, - { - "name": "tmpfs-volume3", - "mountPath": "/run" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "postgres-config", - "mountPath": "/tmp/postgres-config" - }, - { - "name": "postgres-data", - "mountPath": "/var/lib/postgresql/data" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kong-db" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-59882", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-core-command" } }, "spec": { @@ -3037,242 +3173,50 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-metadata", + "path": "/tmp/edgex/secrets/core-command", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-metadata", - "image": "edgexfoundry/core-metadata:2.1.1", + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:2.1.1", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ - { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" - }, { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - } - ], - "resources": {}, - "volumeMounts": [ - { - "name": "edgex-init", - "mountPath": "/edgex-init" - }, - { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-metadata" - } - ], - "imagePullPolicy": "IfNotPresent" - } - ], - "hostname": "edgex-core-metadata" - } - }, - "strategy": {} - } - }, - { - "name": "edgex-kong", - "service": { - "ports": [ - { - "name": "tcp-8000", - "protocol": "TCP", - "port": 8000, - "targetPort": 8000 - }, - { - "name": "tcp-8100", - "protocol": "TCP", - "port": 8100, - "targetPort": 8100 - }, - { - "name": "tcp-8443", - "protocol": "TCP", - "port": 8443, - "targetPort": 8443 - } - ], - "selector": { - "app": "edgex-kong" - } - }, - "deployment": { - "selector": { - "matchLabels": { - "app": "edgex-kong" - } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-kong" - } - }, - "spec": { - "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "edgex-init", - "emptyDir": {} - }, - { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", - "type": "DirectoryOrCreate" - } - }, - { - "name": "postgres-config", - "emptyDir": {} - }, - { - "name": "kong", - "emptyDir": {} - } - ], - "containers": [ - { - "name": "edgex-kong", - "image": "kong:2.5.1", - "ports": [ - { - "name": "tcp-8000", - "containerPort": 8000, - "protocol": "TCP" - }, - { - "name": "tcp-8100", - "containerPort": 8100, - "protocol": "TCP" - }, - { - "name": "tcp-8443", - "containerPort": 8443, - "protocol": "TCP" - } - ], - "envFrom": [ - { - "configMapRef": { - "name": "common-variable-jakarta" - } - } - ], - "env": [ - { - "name": "KONG_PROXY_ACCESS_LOG", - "value": "/dev/stdout" - }, - { - "name": "KONG_DNS_VALID_TTL", - "value": "1" - }, - { - "name": "KONG_DATABASE", - "value": "postgres" - }, - { - "name": "KONG_ADMIN_ACCESS_LOG", - "value": "/dev/stdout" - }, - { - "name": "KONG_SSL_CIPHER_SUITE", - "value": "modern" - }, - { - "name": "KONG_PG_PASSWORD_FILE", - "value": "/tmp/postgres-config/.pgpassword" - }, - { - "name": "KONG_ADMIN_LISTEN", - "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" - }, - { - "name": "KONG_STATUS_LISTEN", - "value": "0.0.0.0:8100" - }, - { - "name": "KONG_NGINX_WORKER_PROCESSES", - "value": "1" - }, - { - "name": "KONG_DNS_ORDER", - "value": "LAST,A,CNAME" - }, - { - "name": "KONG_PG_HOST", - "value": "edgex-kong-db" - }, - { - "name": "KONG_ADMIN_ERROR_LOG", - "value": "/dev/stderr" - }, - { - "name": "KONG_PROXY_ERROR_LOG", - "value": "/dev/stderr" + "value": "edgex-core-command" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/tmp" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" - }, - { - "name": "postgres-config", - "mountPath": "/tmp/postgres-config" - }, - { - "name": "kong", - "mountPath": "/usr/local/kong" + "mountPath": "/tmp/edgex/secrets/core-command" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kong" + "hostname": "edgex-core-command" } }, "strategy": {} @@ -3334,7 +3278,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], @@ -3407,7 +3351,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], @@ -3422,31 +3366,31 @@ } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-59986", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-device-rest" } }, "spec": { @@ -3458,45 +3402,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/app-rules-engine", + "path": "/tmp/edgex/secrets/device-rest", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-app-rules-engine", - "image": "edgexfoundry/app-service-configurable:2.1.2", + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:2.1.1", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" - }, { "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" + "value": "edgex-device-rest" } ], "resources": {}, @@ -3507,130 +3439,151 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/app-rules-engine" + "mountPath": "/tmp/edgex/secrets/device-rest" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-vault", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-8200", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 8200, + "targetPort": 8200 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-vault" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-vault" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-vault" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/support-notifications", - "type": "DirectoryOrCreate" - } + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-support-notifications", - "image": "edgexfoundry/support-notifications:2.1.1", + "name": "edgex-vault", + "image": "vault:1.8.4", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-8200", + "containerPort": 8200, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "name": "VAULT_UI", + "value": "true" + }, + { + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" + }, + { + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-notifications" + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-vault" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-59861", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-support-scheduler" } }, "spec": { @@ -3642,33 +3595,41 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/device-rest", + "path": "/tmp/edgex/secrets/support-scheduler", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-device-rest", - "image": "edgexfoundry/device-rest:2.1.1", + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:2.1.1", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-support-scheduler" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" } ], "resources": {}, @@ -3679,54 +3640,68 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-rest" + "mountPath": "/tmp/edgex/secrets/support-scheduler" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-kong", "service": { "ports": [ { - "name": "tcp-5563", + "name": "tcp-8000", "protocol": "TCP", - "port": 5563, - "targetPort": 5563 + "port": 8000, + "targetPort": 8000 }, { - "name": "tcp-59880", + "name": "tcp-8100", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 8100, + "targetPort": 8100 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-kong" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-kong" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-kong" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} @@ -3734,90 +3709,150 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-data", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-data", - "image": "edgexfoundry/core-data:2.1.1", + "name": "edgex-kong", + "image": "kong:2.5.1", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, + "name": "tcp-8000", + "containerPort": 8000, "protocol": "TCP" }, { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-8100", + "containerPort": 8100, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-data" + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" }, { - "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/core-data/secrets-token.json" + "name": "KONG_DNS_VALID_TTL", + "value": "1" + }, + { + "name": "KONG_SSL_CIPHER_SUITE", + "value": "modern" + }, + { + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_NGINX_WORKER_PROCESSES", + "value": "1" + }, + { + "name": "KONG_PG_HOST", + "value": "edgex-kong-db" + }, + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" + }, + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_STATUS_LISTEN", + "value": "0.0.0.0:8100" + }, + { + "name": "KONG_PG_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "KONG_DNS_ORDER", + "value": "LAST,A,CNAME" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-data" + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-kong" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", - "service": { - "ports": [ - { - "name": "tcp-58890", - "protocol": "TCP", - "port": 58890, - "targetPort": 58890 - } - ], - "selector": { - "app": "edgex-sys-mgmt-agent" - } - }, + "name": "edgex-security-proxy-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-security-proxy-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-security-proxy-setup" } }, "spec": { @@ -3826,107 +3861,72 @@ "name": "edgex-init", "emptyDir": {} }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "edgexfoundry/sys-mgmt-agent:2.1.1", - "ports": [ - { - "name": "tcp-58890", - "containerPort": 58890, - "protocol": "TCP" - } - ], + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:2.1.1", "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ { - "name": "METRICSMECHANISM", - "value": "executor" + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" }, { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" }, { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" - } - ], - "resources": {}, - "volumeMounts": [ + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + }, { - "name": "edgex-init", - "mountPath": "/edgex-init" + "name": "ADD_PROXY_ROUTE" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" - } - ], - "imagePullPolicy": "IfNotPresent" - } - ], - "hostname": "edgex-sys-mgmt-agent" - } - }, - "strategy": {} - } - }, - { - "name": "edgex-security-bootstrapper", - "deployment": { - "selector": { - "matchLabels": { - "app": "edgex-security-bootstrapper" - } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-security-bootstrapper" - } - }, - "spec": { - "volumes": [ - { - "name": "edgex-init", - "emptyDir": {} - } - ], - "containers": [ - { - "name": "edgex-security-bootstrapper", - "image": "edgexfoundry/security-bootstrapper:2.1.1", - "envFrom": [ + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, { - "configMapRef": { - "name": "common-variable-jakarta" - } - } - ], - "env": [ + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, { - "name": "EDGEX_USER", - "value": "2002" + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" }, { - "name": "EDGEX_GROUP", - "value": "2001" + "name": "KONGURL_SERVER", + "value": "edgex-kong" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" } ], "resources": {}, @@ -3934,43 +3934,51 @@ { "name": "edgex-init", "mountPath": "/edgex-init" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-bootstrapper" + "hostname": "edgex-security-proxy-setup" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59860", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-support-notifications" } }, "spec": { @@ -3982,41 +3990,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/support-scheduler", + "path": "/tmp/edgex/secrets/support-notifications", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-support-scheduler", - "image": "edgexfoundry/support-scheduler:2.1.1", + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:2.1.1", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-jakarta" + "name": "common-variables" } } ], "env": [ - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" - }, { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "value": "edgex-support-notifications" } ], "resources": {}, @@ -4027,13 +4027,13 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-scheduler" + "mountPath": "/tmp/edgex/secrets/support-notifications" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-support-notifications" } }, "strategy": {} @@ -4046,7 +4046,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-levski", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -4087,226 +4087,192 @@ ], "components": [ { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-kong-db", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-5432", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 5432, + "targetPort": 5432 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-kong-db" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-kong-db" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-kong-db" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/sys-mgmt-agent", - "type": "DirectoryOrCreate" - } + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "postgres-data", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "edgexfoundry/sys-mgmt-agent:2.3.0", + "name": "edgex-kong-db", + "image": "postgres:13.8-alpine", "ports": [ { - "name": "tcp-58890", - "containerPort": 58890, + "name": "tcp-5432", + "containerPort": 5432, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "METRICSMECHANISM", - "value": "executor" + "name": "POSTGRES_USER", + "value": "kong" }, { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "POSTGRES_DB", + "value": "kong" }, { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" + "name": "POSTGRES_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/var/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/run" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-kong-db" } }, "strategy": {} } }, { - "name": "edgex-ui-go", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-4000", + "name": "tcp-59860", "protocol": "TCP", - "port": 4000, - "targetPort": 4000 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-notifications", + "type": "DirectoryOrCreate" + } + } + ], "containers": [ { - "name": "edgex-ui-go", - "image": "edgexfoundry/edgex-ui:2.3.0", + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:2.3.0", "ports": [ { - "name": "tcp-4000", - "containerPort": 4000, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-ui-go" - } - ], - "resources": {}, - "imagePullPolicy": "IfNotPresent" - } - ], - "hostname": "edgex-ui-go" - } - }, - "strategy": {} - } - }, - { - "name": "edgex-device-rest", - "service": { - "ports": [ - { - "name": "tcp-59986", - "protocol": "TCP", - "port": 59986, - "targetPort": 59986 - } - ], - "selector": { - "app": "edgex-device-rest" - } - }, - "deployment": { - "selector": { - "matchLabels": { - "app": "edgex-device-rest" - } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-device-rest" - } - }, - "spec": { - "volumes": [ - { - "name": "edgex-init", - "emptyDir": {} - }, - { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/device-rest", - "type": "DirectoryOrCreate" - } - } - ], - "containers": [ - { - "name": "edgex-device-rest", - "image": "edgexfoundry/device-rest:2.3.0", - "ports": [ - { - "name": "tcp-59986", - "containerPort": 59986, - "protocol": "TCP" - } - ], - "envFrom": [ - { - "configMapRef": { - "name": "common-variable-levski" - } - } - ], - "env": [ - { - "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-support-notifications" } ], "resources": {}, @@ -4317,50 +4283,44 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-rest" + "mountPath": "/tmp/edgex/secrets/support-notifications" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-59701", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-app-rules-engine" } }, "spec": { @@ -4372,42 +4332,45 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-data", + "path": "/tmp/edgex/secrets/app-rules-engine", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-data", - "image": "edgexfoundry/core-data:2.3.0", + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:2.3.1", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, { "name": "SERVICE_HOST", - "value": "edgex-core-data" + "value": "edgex-app-rules-engine" }, { - "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/core-data/secrets-token.json" + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" } ], "resources": {}, @@ -4418,99 +4381,76 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-data" + "mountPath": "/tmp/edgex/secrets/app-rules-engine" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-4000", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 4000, + "targetPort": 4000 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-ui-go" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-ui-go" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-ui-go" } }, "spec": { - "volumes": [ - { - "name": "edgex-init", - "emptyDir": {} - }, - { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/support-notifications", - "type": "DirectoryOrCreate" - } - } - ], "containers": [ { - "name": "edgex-support-notifications", - "image": "edgexfoundry/support-notifications:2.3.0", + "name": "edgex-ui-go", + "image": "edgexfoundry/edgex-ui:2.3.0", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-4000", + "containerPort": 4000, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-ui-go" } ], "resources": {}, - "volumeMounts": [ - { - "name": "edgex-init", - "mountPath": "/edgex-init" - }, - { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-notifications" - } - ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-ui-go" } }, "strategy": {} @@ -4572,11 +4512,15 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-core-command" + }, { "name": "MESSAGEQUEUE_EXTERNAL_URL", "value": "tcp://edgex-mqtt-broker:1883" @@ -4584,10 +4528,6 @@ { "name": "MESSAGEQUEUE_INTERNAL_HOST", "value": "edgex-redis" - }, - { - "name": "SERVICE_HOST", - "value": "edgex-core-command" } ], "resources": {}, @@ -4611,41 +4551,41 @@ } }, { - "name": "edgex-core-consul", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-6379", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-redis" } }, "spec": { "volumes": [ { - "name": "consul-config", + "name": "tmpfs-volume1", "emptyDir": {} }, { - "name": "consul-data", + "name": "db-data", "emptyDir": {} }, { @@ -4653,118 +4593,90 @@ "emptyDir": {} }, { - "name": "consul-acl-token", + "name": "redis-config", "emptyDir": {} }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/edgex-consul", + "path": "/tmp/edgex/secrets/security-bootstrapper-redis", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-consul", - "image": "consul:1.13.2", + "name": "edgex-redis", + "image": "redis:7.0.5-alpine", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", - "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" - }, - { - "name": "STAGEGATE_REGISTRY_ACL_MANAGEMENTTOKENPATH", - "value": "/tmp/edgex/secrets/consul-acl-token/mgmt_token.json" - }, - { - "name": "ADD_REGISTRY_ACL_ROLES" - }, - { - "name": "EDGEX_USER", - "value": "2002" - }, - { - "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", - "value": "/consul/config/consul_acl_done" + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" }, { - "name": "EDGEX_GROUP", - "value": "2001" + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" } ], "resources": {}, "volumeMounts": [ { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "tmpfs-volume1", + "mountPath": "/run" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "db-data", + "mountPath": "/data" }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" + "name": "redis-config", + "mountPath": "/run/redis/conf" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/edgex-consul" + "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", - "service": { - "ports": [ - { - "name": "tcp-59900", - "protocol": "TCP", - "port": 59900, - "targetPort": 59900 - } - ], - "selector": { - "app": "edgex-device-virtual" - } - }, + "name": "edgex-security-proxy-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-security-proxy-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-security-proxy-setup" } }, "spec": { @@ -4773,36 +4685,72 @@ "name": "edgex-init", "emptyDir": {} }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/device-virtual", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-device-virtual", - "image": "edgexfoundry/device-virtual:2.3.0", - "ports": [ - { - "name": "tcp-59900", - "containerPort": 59900, - "protocol": "TCP" - } - ], + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:2.3.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" + }, + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + }, + { + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "KONGURL_SERVER", + "value": "edgex-kong" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, + { + "name": "ADD_PROXY_ROUTE" } ], "resources": {}, @@ -4811,258 +4759,279 @@ "name": "edgex-init", "mountPath": "/edgex-init" }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-virtual" + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-security-proxy-setup" } }, "strategy": {} } }, { - "name": "edgex-redis", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-59861", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-support-scheduler" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "db-data", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, - { - "name": "redis-config", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "path": "/tmp/edgex/secrets/support-scheduler", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-redis", - "image": "redis:7.0.5-alpine", + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:2.3.0", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "DATABASECONFIG_PATH", - "value": "/run/redis/conf" + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" }, { - "name": "DATABASECONFIG_NAME", - "value": "redis.conf" + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "db-data", - "mountPath": "/data" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, - { - "name": "redis-config", - "mountPath": "/run/redis/conf" - }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" + "mountPath": "/tmp/edgex/secrets/support-scheduler" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-8500", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-consul" } }, "spec": { "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/app-rules-engine", + "path": "/tmp/edgex/secrets/edgex-consul", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-app-rules-engine", - "image": "edgexfoundry/app-service-configurable:2.3.1", + "name": "edgex-core-consul", + "image": "consul:1.13.2", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_PROFILE", - "value": "rules-engine" + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" }, { - "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" + "name": "ADD_REGISTRY_ACL_ROLES" }, { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" + "name": "EDGEX_USER", + "value": "2002" }, { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" + "name": "STAGEGATE_REGISTRY_ACL_MANAGEMENTTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/mgmt_token.json" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" } ], "resources": {}, "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/app-rules-engine" + "mountPath": "/tmp/edgex/secrets/edgex-consul" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-kuiper", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59720", + "name": "tcp-5563", "protocol": "TCP", - "port": 59720, - "targetPort": 59720 + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "spec": { @@ -5072,80 +5041,44 @@ "emptyDir": {} }, { - "name": "kuiper-data", - "emptyDir": {} - }, + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-data", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ { - "name": "kuiper-connections", - "emptyDir": {} - }, - { - "name": "kuiper-sources", - "emptyDir": {} - } - ], - "containers": [ - { - "name": "edgex-kuiper", - "image": "lfedge/ekuiper:1.7.1-alpine", + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:2.3.0", "ports": [ { - "name": "tcp-59720", - "containerPort": 59720, + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" + }, + { + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/core-data/secrets-token.json" }, { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", - "value": "edgex-redis" + "name": "SERVICE_HOST", + "value": "edgex-core-data" } ], "resources": {}, @@ -5155,53 +5088,45 @@ "mountPath": "/edgex-init" }, { - "name": "kuiper-data", - "mountPath": "/kuiper/data" - }, - { - "name": "kuiper-connections", - "mountPath": "/kuiper/etc/connections" - }, - { - "name": "kuiper-sources", - "mountPath": "/kuiper/etc/sources" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59900", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "spec": { @@ -5213,41 +5138,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/support-scheduler", + "path": "/tmp/edgex/secrets/device-virtual", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-support-scheduler", - "image": "edgexfoundry/support-scheduler:2.3.0", + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:2.3.0", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" - }, { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "value": "edgex-device-virtual" } ], "resources": {}, @@ -5258,90 +5175,151 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-scheduler" + "mountPath": "/tmp/edgex/secrets/device-virtual" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-security-bootstrapper", + "name": "edgex-vault", + "service": { + "ports": [ + { + "name": "tcp-8200", + "protocol": "TCP", + "port": 8200, + "targetPort": 8200 + } + ], + "selector": { + "app": "edgex-vault" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-bootstrapper" + "app": "edgex-vault" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-bootstrapper" + "app": "edgex-vault" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} + }, + { + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-security-bootstrapper", - "image": "edgexfoundry/security-bootstrapper:2.3.0", + "name": "edgex-vault", + "image": "vault:1.11.4", + "ports": [ + { + "name": "tcp-8200", + "containerPort": 8200, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_USER", - "value": "2002" + "name": "VAULT_UI", + "value": "true" }, { - "name": "EDGEX_GROUP", - "value": "2001" + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" + }, + { + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, { "name": "edgex-init", "mountPath": "/edgex-init" + }, + { + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-bootstrapper" + "hostname": "edgex-vault" } }, "strategy": {} } }, { - "name": "edgex-security-proxy-setup", + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-59986", + "protocol": "TCP", + "port": 59986, + "targetPort": 59986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-device-rest" } }, "spec": { @@ -5350,243 +5328,210 @@ "name": "edgex-init", "emptyDir": {} }, - { - "name": "consul-acl-token", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", + "path": "/tmp/edgex/secrets/device-rest", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-security-proxy-setup", - "image": "edgexfoundry/security-proxy-setup:2.3.0", + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:2.3.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "ROUTES_SYS_MGMT_AGENT_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "ROUTES_CORE_DATA_HOST", - "value": "edgex-core-data" - }, - { - "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", - "value": "edgex-support-notifications" - }, - { - "name": "ROUTES_SUPPORT_SCHEDULER_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "ROUTES_CORE_METADATA_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "ADD_PROXY_ROUTE" - }, - { - "name": "KONGURL_SERVER", - "value": "edgex-kong" - }, + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "volumeMounts": [ { - "name": "ROUTES_CORE_CONSUL_HOST", - "value": "edgex-core-consul" - }, - { - "name": "ROUTES_RULES_ENGINE_HOST", - "value": "edgex-kuiper" - }, - { - "name": "ROUTES_DEVICE_VIRTUAL_HOST", - "value": "device-virtual" - }, - { - "name": "ROUTES_CORE_COMMAND_HOST", - "value": "edgex-core-command" - } - ], - "resources": {}, - "volumeMounts": [ - { - "name": "edgex-init", - "mountPath": "/edgex-init" - }, - { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" + "name": "edgex-init", + "mountPath": "/edgex-init" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + "mountPath": "/tmp/edgex/secrets/device-rest" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-proxy-setup" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-vault", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-8200", + "name": "tcp-59720", "protocol": "TCP", - "port": 8200, - "targetPort": 8200 + "port": 59720, + "targetPort": 59720 } ], "selector": { - "app": "edgex-vault" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-vault" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-vault" + "app": "edgex-kuiper" } }, "spec": { "volumes": [ { - "name": "tmpfs-volume1", + "name": "edgex-init", "emptyDir": {} }, { - "name": "edgex-init", + "name": "kuiper-data", "emptyDir": {} }, { - "name": "vault-file", + "name": "kuiper-connections", "emptyDir": {} }, { - "name": "vault-logs", + "name": "kuiper-sources", "emptyDir": {} } ], "containers": [ { - "name": "edgex-vault", - "image": "vault:1.11.4", + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.7.1-alpine", "ports": [ { - "name": "tcp-8200", - "containerPort": 8200, + "name": "tcp-59720", + "containerPort": 59720, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "VAULT_CONFIG_DIR", - "value": "/vault/config" + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" }, { - "name": "VAULT_ADDR", - "value": "http://edgex-vault:8200" + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" }, { - "name": "VAULT_UI", - "value": "true" + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", + "value": "redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" + }, + { + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, + { + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" + }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/vault/config" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "vault-file", - "mountPath": "/vault/file" + "name": "kuiper-data", + "mountPath": "/kuiper/data" }, { - "name": "vault-logs", - "mountPath": "/vault/logs" + "name": "kuiper-connections", + "mountPath": "/kuiper/etc/connections" + }, + { + "name": "kuiper-sources", + "mountPath": "/kuiper/etc/sources" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-vault" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-kong", - "service": { - "ports": [ - { - "name": "tcp-8000", - "protocol": "TCP", - "port": 8000, - "targetPort": 8000 - }, - { - "name": "tcp-8100", - "protocol": "TCP", - "port": 8100, - "targetPort": 8100 - }, - { - "name": "tcp-8443", - "protocol": "TCP", - "port": 8443, - "targetPort": 8443 - } - ], - "selector": { - "app": "edgex-kong" - } - }, + "name": "edgex-security-secretstore-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-kong" + "app": "edgex-security-secretstore-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kong" + "app": "edgex-security-secretstore-setup" } }, "spec": { @@ -5606,99 +5551,57 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", + "path": "/tmp/edgex/secrets", "type": "DirectoryOrCreate" } }, { - "name": "postgres-config", + "name": "kong", "emptyDir": {} }, { - "name": "kong", + "name": "kuiper-sources", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "vault-config", "emptyDir": {} } ], "containers": [ { - "name": "edgex-kong", - "image": "kong:2.8.1", - "ports": [ - { - "name": "tcp-8000", - "containerPort": 8000, - "protocol": "TCP" - }, - { - "name": "tcp-8100", - "containerPort": 8100, - "protocol": "TCP" - }, - { - "name": "tcp-8443", - "containerPort": 8443, - "protocol": "TCP" - } - ], + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:2.3.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "KONG_ADMIN_ACCESS_LOG", - "value": "/dev/stdout" - }, - { - "name": "KONG_DNS_ORDER", - "value": "LAST,A,CNAME" - }, - { - "name": "KONG_DNS_VALID_TTL", - "value": "1" - }, - { - "name": "KONG_PROXY_ERROR_LOG", - "value": "/dev/stderr" - }, - { - "name": "KONG_NGINX_WORKER_PROCESSES", - "value": "1" - }, - { - "name": "KONG_PG_PASSWORD_FILE", - "value": "/tmp/postgres-config/.pgpassword" - }, - { - "name": "KONG_PG_HOST", - "value": "edgex-kong-db" - }, - { - "name": "KONG_SSL_CIPHER_SUITE", - "value": "modern" - }, - { - "name": "KONG_DATABASE", - "value": "postgres" + "name": "EDGEX_USER", + "value": "2002" }, { - "name": "KONG_PROXY_ACCESS_LOG", - "value": "/dev/stdout" + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" }, { - "name": "KONG_STATUS_LISTEN", - "value": "0.0.0.0:8100" + "name": "ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],message-bus[device-rest],redisdb[device-virtual],message-bus[device-virtual]" }, { - "name": "KONG_ADMIN_ERROR_LOG", - "value": "/dev/stderr" + "name": "ADD_SECRETSTORE_TOKENS" }, { - "name": "KONG_ADMIN_LISTEN", - "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" + "name": "EDGEX_GROUP", + "value": "2001" } ], "resources": {}, @@ -5709,7 +5612,7 @@ }, { "name": "tmpfs-volume2", - "mountPath": "/tmp" + "mountPath": "/vault" }, { "name": "edgex-init", @@ -5717,174 +5620,154 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + "mountPath": "/tmp/edgex/secrets" }, { - "name": "postgres-config", - "mountPath": "/tmp/postgres-config" + "name": "kong", + "mountPath": "/tmp/kong" }, { - "name": "kong", - "mountPath": "/usr/local/kong" + "name": "kuiper-sources", + "mountPath": "/tmp/kuiper" + }, + { + "name": "kuiper-connections", + "mountPath": "/tmp/kuiper-connections" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kong" + "hostname": "edgex-security-secretstore-setup" } }, "strategy": {} } }, { - "name": "edgex-kong-db", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-5432", + "name": "tcp-59881", "protocol": "TCP", - "port": 5432, - "targetPort": 5432 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-kong-db" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kong-db" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kong-db" + "app": "edgex-core-metadata" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "tmpfs-volume3", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "postgres-config", - "emptyDir": {} - }, - { - "name": "postgres-data", - "emptyDir": {} + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-metadata", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-kong-db", - "image": "postgres:13.8-alpine", + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:2.3.0", "ports": [ { - "name": "tcp-5432", - "containerPort": 5432, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "POSTGRES_USER", - "value": "kong" - }, - { - "name": "POSTGRES_PASSWORD_FILE", - "value": "/tmp/postgres-config/.pgpassword" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" }, { - "name": "POSTGRES_DB", - "value": "kong" + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/var/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/tmp" - }, - { - "name": "tmpfs-volume3", - "mountPath": "/run" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "postgres-config", - "mountPath": "/tmp/postgres-config" - }, - { - "name": "postgres-data", - "mountPath": "/var/lib/postgresql/data" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-metadata" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kong-db" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-security-secretstore-setup", + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-secretstore-setup" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-secretstore-setup" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} @@ -5892,133 +5775,113 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets", + "path": "/tmp/edgex/secrets/sys-mgmt-agent", "type": "DirectoryOrCreate" } - }, - { - "name": "kong", - "emptyDir": {} - }, - { - "name": "kuiper-sources", - "emptyDir": {} - }, - { - "name": "kuiper-connections", - "emptyDir": {} - }, - { - "name": "vault-config", - "emptyDir": {} } ], "containers": [ { - "name": "edgex-security-secretstore-setup", - "image": "edgexfoundry/security-secretstore-setup:2.3.0", + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/sys-mgmt-agent:2.3.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_GROUP", - "value": "2001" - }, - { - "name": "ADD_KNOWN_SECRETS", - "value": "redisdb[app-rules-engine],redisdb[device-rest],message-bus[device-rest],redisdb[device-virtual],message-bus[device-virtual]" - }, - { - "name": "ADD_SECRETSTORE_TOKENS" + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" }, { - "name": "EDGEX_USER", - "value": "2002" + "name": "METRICSMECHANISM", + "value": "executor" }, { - "name": "SECUREMESSAGEBUS_TYPE", - "value": "redis" + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/vault" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets" - }, - { - "name": "kong", - "mountPath": "/tmp/kong" - }, - { - "name": "kuiper-sources", - "mountPath": "/tmp/kuiper" - }, - { - "name": "kuiper-connections", - "mountPath": "/tmp/kuiper-connections" - }, - { - "name": "vault-config", - "mountPath": "/vault/config" + "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-secretstore-setup" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-kong", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-8000", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8100", + "protocol": "TCP", + "port": 8100, + "targetPort": 8100 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-kong" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-kong" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-kong" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} @@ -6026,54 +5889,191 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-metadata", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-metadata", - "image": "edgexfoundry/core-metadata:2.3.0", + "name": "edgex-kong", + "image": "kong:2.8.1", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8100", + "containerPort": 8100, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-levski" + "name": "common-variables" } } ], "env": [ { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "name": "KONG_SSL_CIPHER_SUITE", + "value": "modern" }, { - "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - } - ], - "resources": {}, - "volumeMounts": [ + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" + }, + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_DNS_ORDER", + "value": "LAST,A,CNAME" + }, + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_PG_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "KONG_NGINX_WORKER_PROCESSES", + "value": "1" + }, + { + "name": "KONG_PG_HOST", + "value": "edgex-kong-db" + }, + { + "name": "KONG_DNS_VALID_TTL", + "value": "1" + }, + { + "name": "KONG_STATUS_LISTEN", + "value": "0.0.0.0:8100" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-metadata" + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-kong" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrapper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:2.3.0", + "envFrom": [ + { + "configMapRef": { + "name": "common-variables" + } + } + ], + "env": [ + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "EDGEX_USER", + "value": "2002" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-security-bootstrapper" } }, "strategy": {} @@ -6086,7 +6086,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-minnesota", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -6110,190 +6110,118 @@ ], "components": [ { - "name": "edgex-security-secretstore-setup", + "name": "edgex-kuiper", + "service": { + "ports": [ + { + "name": "tcp-59720", + "protocol": "TCP", + "port": 59720, + "targetPort": 59720 + } + ], + "selector": { + "app": "edgex-kuiper" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-secretstore-setup" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-secretstore-setup" + "app": "edgex-kuiper" } }, "spec": { "volumes": [ { - "name": "tmpfs-volume1", + "name": "edgex-init", "emptyDir": {} }, { - "name": "tmpfs-volume2", + "name": "kuiper-data", "emptyDir": {} }, { - "name": "edgex-init", + "name": "kuiper-connections", "emptyDir": {} }, - { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets", - "type": "DirectoryOrCreate" - } - }, { "name": "kuiper-sources", "emptyDir": {} }, { - "name": "kuiper-connections", - "emptyDir": {} - }, - { - "name": "vault-config", + "name": "kuiper-log", "emptyDir": {} } ], "containers": [ { - "name": "edgex-security-secretstore-setup", - "image": "edgexfoundry/security-secretstore-setup:3.0.0", + "name": "edgex-kuiper", + "image": "lfedge/ekuiper:1.9.2-alpine", + "ports": [ + { + "name": "tcp-59720", + "containerPort": 59720, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "SECUREMESSAGEBUS_TYPE", + "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", "value": "redis" }, { - "name": "EDGEX_ADD_SECRETSTORE_TOKENS" - }, - { - "name": "EDGEX_ADD_KNOWN_SECRETS", - "value": "redisdb[app-rules-engine],redisdb[device-rest],message-bus[device-rest],redisdb[device-virtual],message-bus[device-virtual]" - }, - { - "name": "EDGEX_USER", - "value": "2002" + "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", + "value": "6379" }, { - "name": "EDGEX_GROUP", - "value": "2001" - } - ], - "resources": {}, - "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" + "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", + "value": "edgex-redis" }, { - "name": "tmpfs-volume2", - "mountPath": "/vault" + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" }, { - "name": "edgex-init", - "mountPath": "/edgex-init" + "name": "EDGEX__DEFAULT__TOPIC", + "value": "edgex/rules-events" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets" + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" }, { - "name": "kuiper-sources", - "mountPath": "/tmp/kuiper" + "name": "EDGEX__DEFAULT__PORT", + "value": "6379" }, { - "name": "kuiper-connections", - "mountPath": "/tmp/kuiper-connections" + "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", + "value": "redis" }, { - "name": "vault-config", - "mountPath": "/vault/config" - } - ], - "imagePullPolicy": "IfNotPresent" - } - ], - "hostname": "edgex-security-secretstore-setup" - } - }, - "strategy": {} - } - }, - { - "name": "edgex-core-data", - "service": { - "ports": [ - { - "name": "tcp-59880", - "protocol": "TCP", - "port": 59880, - "targetPort": 59880 - } - ], - "selector": { - "app": "edgex-core-data" - } - }, - "deployment": { - "selector": { - "matchLabels": { - "app": "edgex-core-data" - } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-core-data" - } - }, - "spec": { - "volumes": [ - { - "name": "edgex-init", - "emptyDir": {} - }, - { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/core-data", - "type": "DirectoryOrCreate" - } - } - ], - "containers": [ - { - "name": "edgex-core-data", - "image": "edgexfoundry/core-data:3.0.0", - "ports": [ - { - "name": "tcp-59880", - "containerPort": 59880, - "protocol": "TCP" - } - ], - "envFrom": [ + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, { - "configMapRef": { - "name": "common-variable-minnesota" - } - } - ], - "env": [ + "name": "EDGEX__DEFAULT__TYPE", + "value": "redis" + }, { - "name": "SERVICE_HOST", - "value": "edgex-core-data" + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "redis" } ], "resources": {}, @@ -6303,45 +6231,57 @@ "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-data" + "name": "kuiper-data", + "mountPath": "/kuiper/data" + }, + { + "name": "kuiper-connections", + "mountPath": "/kuiper/etc/connections" + }, + { + "name": "kuiper-sources", + "mountPath": "/kuiper/etc/sources" + }, + { + "name": "kuiper-log", + "mountPath": "/kuiper/log" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-proxy-auth", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59842", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59842, + "targetPort": 59842 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-proxy-auth" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-proxy-auth" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-proxy-auth" } }, "spec": { @@ -6353,41 +6293,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/support-scheduler", + "path": "/tmp/edgex/secrets/security-proxy-auth", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-support-scheduler", - "image": "edgexfoundry/support-scheduler:3.0.0", + "name": "edgex-proxy-auth", + "image": "edgexfoundry/security-proxy-auth:3.0.0", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59842", + "containerPort": 59842, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" - }, { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" + "value": "edgex-proxy-auth" } ], "resources": {}, @@ -6398,177 +6330,173 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-scheduler" + "mountPath": "/tmp/edgex/secrets/security-proxy-auth" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-proxy-auth" } }, "strategy": {} } }, { - "name": "edgex-security-proxy-setup", + "name": "edgex-core-consul", + "service": { + "ports": [ + { + "name": "tcp-8500", + "protocol": "TCP", + "port": 8500, + "targetPort": 8500 + } + ], + "selector": { + "app": "edgex-core-consul" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-core-consul" } }, "spec": { "volumes": [ { - "name": "edgex-init", + "name": "consul-config", "emptyDir": {} }, { - "name": "vault-config", + "name": "consul-data", "emptyDir": {} }, { - "name": "nginx-templates", + "name": "edgex-init", "emptyDir": {} }, { - "name": "nginx-tls", + "name": "consul-acl-token", "emptyDir": {} }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", + "path": "/tmp/edgex/secrets/edgex-consul", "type": "DirectoryOrCreate" } - }, - { - "name": "consul-acl-token", - "emptyDir": {} } ], "containers": [ { - "name": "edgex-security-proxy-setup", - "image": "edgexfoundry/security-proxy-setup:3.0.0", + "name": "edgex-core-consul", + "image": "hashicorp/consul:1.15.2", + "ports": [ + { + "name": "tcp-8500", + "containerPort": 8500, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "ROUTES_RULES_ENGINE_HOST", - "value": "edgex-kuiper" - }, - { - "name": "ROUTES_CORE_CONSUL_HOST", - "value": "edgex-core-consul" - }, - { - "name": "ROUTES_SYS_MGMT_AGENT_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", - "value": "edgex-support-notifications" - }, - { - "name": "ROUTES_CORE_DATA_HOST", - "value": "edgex-core-data" + "name": "EDGEX_GROUP", + "value": "2001" }, { - "name": "ROUTES_CORE_METADATA_HOST", - "value": "edgex-core-metadata" + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" }, { - "name": "ROUTES_DEVICE_VIRTUAL_HOST", - "value": "device-virtual" + "name": "EDGEX_ADD_REGISTRY_ACL_ROLES" }, { - "name": "ROUTES_CORE_COMMAND_HOST", - "value": "edgex-core-command" + "name": "EDGEX_USER", + "value": "2002" }, { - "name": "ROUTES_SUPPORT_SCHEDULER_HOST", - "value": "edgex-support-scheduler" + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" }, { - "name": "EDGEX_ADD_PROXY_ROUTE" + "name": "STAGEGATE_REGISTRY_ACL_MANAGEMENTTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/mgmt_token.json" } ], "resources": {}, "volumeMounts": [ { - "name": "edgex-init", - "mountPath": "/edgex-init" + "name": "consul-config", + "mountPath": "/consul/config" }, { - "name": "vault-config", - "mountPath": "/vault/config" + "name": "consul-data", + "mountPath": "/consul/data" }, { - "name": "nginx-templates", - "mountPath": "/etc/nginx/templates" + "name": "edgex-init", + "mountPath": "/edgex-init" }, { - "name": "nginx-tls", - "mountPath": "/etc/ssl/nginx" + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" - }, - { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" + "mountPath": "/tmp/edgex/secrets/edgex-consul" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-proxy-setup" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-device-rest", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-59986", + "name": "tcp-59880", "protocol": "TCP", - "port": 59986, - "targetPort": 59986 + "port": 59880, + "targetPort": 59880 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-core-data" } }, "spec": { @@ -6580,33 +6508,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/device-rest", + "path": "/tmp/edgex/secrets/core-data", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-device-rest", - "image": "edgexfoundry/device-rest:3.0.0", + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:3.0.0", "ports": [ { - "name": "tcp-59986", - "containerPort": 59986, + "name": "tcp-59880", + "containerPort": 59880, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-core-data" } ], "resources": {}, @@ -6617,131 +6545,59 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-rest" + "mountPath": "/tmp/edgex/secrets/core-data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-kuiper", - "service": { - "ports": [ - { - "name": "tcp-59720", - "protocol": "TCP", - "port": 59720, - "targetPort": 59720 - } - ], - "selector": { - "app": "edgex-kuiper" - } - }, + "name": "edgex-security-bootstrapper", "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-security-bootstrapper" } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-kuiper" - } - }, - "spec": { - "volumes": [ - { - "name": "edgex-init", - "emptyDir": {} - }, - { - "name": "kuiper-data", - "emptyDir": {} - }, - { - "name": "kuiper-connections", - "emptyDir": {} - }, - { - "name": "kuiper-sources", - "emptyDir": {} - }, + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrapper" + } + }, + "spec": { + "volumes": [ { - "name": "kuiper-log", + "name": "edgex-init", "emptyDir": {} } ], "containers": [ { - "name": "edgex-kuiper", - "image": "lfedge/ekuiper:1.9.2-alpine", - "ports": [ - { - "name": "tcp-59720", - "containerPort": 59720, - "protocol": "TCP" - } - ], + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:3.0.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX__DEFAULT__TYPE", - "value": "redis" - }, - { - "name": "EDGEX__DEFAULT__PORT", - "value": "6379" - }, - { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "edgex/rules-events" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PROTOCOL", - "value": "redis" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__PORT", - "value": "6379" - }, - { - "name": "CONNECTION__EDGEX__REDISMSGBUS__TYPE", - "value": "redis" - }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" + "name": "EDGEX_USER", + "value": "2002" }, { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "redis" + "name": "EDGEX_GROUP", + "value": "2001" } ], "resources": {}, @@ -6749,358 +6605,305 @@ { "name": "edgex-init", "mountPath": "/edgex-init" - }, - { - "name": "kuiper-data", - "mountPath": "/kuiper/data" - }, - { - "name": "kuiper-connections", - "mountPath": "/kuiper/etc/connections" - }, - { - "name": "kuiper-sources", - "mountPath": "/kuiper/etc/sources" - }, - { - "name": "kuiper-log", - "mountPath": "/kuiper/log" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kuiper" + "hostname": "edgex-security-bootstrapper" } }, "strategy": {} } }, { - "name": "edgex-ui-go", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-4000", + "name": "tcp-59860", "protocol": "TCP", - "port": 4000, - "targetPort": 4000 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-ui-go" + "app": "edgex-support-notifications" } }, "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-notifications", + "type": "DirectoryOrCreate" + } + } + ], "containers": [ { - "name": "edgex-ui-go", - "image": "edgexfoundry/edgex-ui:3.0.0", + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:3.0.0", "ports": [ { - "name": "tcp-4000", - "containerPort": 4000, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-ui-go" + "value": "edgex-support-notifications" } ], "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-notifications" + } + ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-ui-go" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-vault", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-8200", + "name": "tcp-59861", "protocol": "TCP", - "port": 8200, - "targetPort": 8200 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-vault" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-vault" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-vault" + "app": "edgex-support-scheduler" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "vault-file", - "emptyDir": {} - }, - { - "name": "vault-logs", - "emptyDir": {} + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/support-scheduler", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-vault", - "image": "hashicorp/vault:1.13.2", + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:3.0.0", "ports": [ { - "name": "tcp-8200", - "containerPort": 8200, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "VAULT_UI", - "value": "true" + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" }, { - "name": "VAULT_ADDR", - "value": "http://edgex-vault:8200" + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" }, { - "name": "VAULT_CONFIG_DIR", - "value": "/vault/config" + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/vault/config" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "vault-file", - "mountPath": "/vault/file" - }, - { - "name": "vault-logs", - "mountPath": "/vault/logs" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/support-scheduler" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-vault" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-core-consul", - "service": { - "ports": [ - { - "name": "tcp-8500", - "protocol": "TCP", - "port": 8500, - "targetPort": 8500 - } - ], - "selector": { - "app": "edgex-core-consul" - } - }, + "name": "edgex-core-common-config-bootstrapper", "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-core-common-config-bootstrapper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-core-common-config-bootstrapper" } }, "spec": { "volumes": [ - { - "name": "consul-config", - "emptyDir": {} - }, - { - "name": "consul-data", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, - { - "name": "consul-acl-token", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/edgex-consul", + "path": "/tmp/edgex/secrets/core-common-config-bootstrapper", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-consul", - "image": "hashicorp/consul:1.15.2", - "ports": [ - { - "name": "tcp-8500", - "containerPort": 8500, - "protocol": "TCP" - } - ], + "name": "edgex-core-common-config-bootstrapper", + "image": "edgexfoundry/core-common-config-bootstrapper:3.0.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_GROUP", - "value": "2001" - }, - { - "name": "STAGEGATE_REGISTRY_ACL_MANAGEMENTTOKENPATH", - "value": "/tmp/edgex/secrets/consul-acl-token/mgmt_token.json" + "name": "ALL_SERVICES_MESSAGEBUS_HOST", + "value": "edgex-redis" }, { - "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", - "value": "/consul/config/consul_acl_done" + "name": "ALL_SERVICES_DATABASE_HOST", + "value": "edgex-redis" }, - { - "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", - "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + { + "name": "APP_SERVICES_CLIENTS_CORE_METADATA_HOST", + "value": "edgex-core-metadata" }, { - "name": "EDGEX_USER", - "value": "2002" + "name": "DEVICE_SERVICES_CLIENTS_CORE_METADATA_HOST", + "value": "edgex-core-metadata" }, { - "name": "EDGEX_ADD_REGISTRY_ACL_ROLES" + "name": "ALL_SERVICES_REGISTRY_HOST", + "value": "edgex-core-consul" } ], "resources": {}, "volumeMounts": [ - { - "name": "consul-config", - "mountPath": "/consul/config" - }, - { - "name": "consul-data", - "mountPath": "/consul/data" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, - { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" - }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/edgex-consul" + "mountPath": "/tmp/edgex/secrets/core-common-config-bootstrapper" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-core-common-config-bootstrapper" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-59701", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-app-rules-engine" } }, "spec": { @@ -7112,33 +6915,37 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-metadata", + "path": "/tmp/edgex/secrets/app-rules-engine", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-metadata", - "image": "edgexfoundry/core-metadata:3.0.0", + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:3.0.0", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" + "value": "edgex-app-rules-engine" } ], "resources": {}, @@ -7149,31 +6956,44 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-metadata" + "mountPath": "/tmp/edgex/secrets/app-rules-engine" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-security-bootstrapper", + "name": "edgex-core-metadata", + "service": { + "ports": [ + { + "name": "tcp-59881", + "protocol": "TCP", + "port": 59881, + "targetPort": 59881 + } + ], + "selector": { + "app": "edgex-core-metadata" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-bootstrapper" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-bootstrapper" + "app": "edgex-core-metadata" } }, "spec": { @@ -7181,27 +7001,37 @@ { "name": "edgex-init", "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-metadata", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-security-bootstrapper", - "image": "edgexfoundry/security-bootstrapper:3.0.0", + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:3.0.0", + "ports": [ + { + "name": "tcp-59881", + "containerPort": 59881, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_GROUP", - "value": "2001" - }, - { - "name": "EDGEX_USER", - "value": "2002" + "name": "SERVICE_HOST", + "value": "edgex-core-metadata" } ], "resources": {}, @@ -7209,246 +7039,217 @@ { "name": "edgex-init", "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/core-metadata" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-bootstrapper" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-proxy-auth", + "name": "edgex-vault", "service": { "ports": [ { - "name": "tcp-59842", + "name": "tcp-8200", "protocol": "TCP", - "port": 59842, - "targetPort": 59842 + "port": 8200, + "targetPort": 8200 } ], "selector": { - "app": "edgex-proxy-auth" + "app": "edgex-vault" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-proxy-auth" + "app": "edgex-vault" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-proxy-auth" + "app": "edgex-vault" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-auth", - "type": "DirectoryOrCreate" - } + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-proxy-auth", - "image": "edgexfoundry/security-proxy-auth:3.0.0", + "name": "edgex-vault", + "image": "hashicorp/vault:1.13.2", "ports": [ { - "name": "tcp-59842", - "containerPort": 59842, + "name": "tcp-8200", + "containerPort": 8200, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-proxy-auth" + "name": "VAULT_UI", + "value": "true" + }, + { + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" + }, + { + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/vault/config" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-auth" + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-proxy-auth" + "hostname": "edgex-vault" } }, "strategy": {} } }, { - "name": "edgex-nginx", + "name": "edgex-ui-go", "service": { "ports": [ { - "name": "tcp-8443", + "name": "tcp-4000", "protocol": "TCP", - "port": 8443, - "targetPort": 8443 + "port": 4000, + "targetPort": 4000 } ], "selector": { - "app": "edgex-nginx" + "app": "edgex-ui-go" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-nginx" + "app": "edgex-ui-go" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-nginx" + "app": "edgex-ui-go" } }, "spec": { - "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "tmpfs-volume3", - "emptyDir": {} - }, - { - "name": "tmpfs-volume4", - "emptyDir": {} - }, - { - "name": "edgex-init", - "emptyDir": {} - }, - { - "name": "nginx-templates", - "emptyDir": {} - }, - { - "name": "nginx-tls", - "emptyDir": {} - } - ], "containers": [ { - "name": "edgex-nginx", - "image": "nginx:1.24.0-alpine-slim", + "name": "edgex-ui-go", + "image": "edgexfoundry/edgex-ui:3.0.0", "ports": [ { - "name": "tcp-8443", - "containerPort": 8443, + "name": "tcp-4000", + "containerPort": 4000, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], - "resources": {}, - "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/etc/nginx/conf.d" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/var/cache/nginx" - }, - { - "name": "tmpfs-volume3", - "mountPath": "/var/log/nginx" - }, - { - "name": "tmpfs-volume4", - "mountPath": "/var/run" - }, - { - "name": "edgex-init", - "mountPath": "/edgex-init" - }, - { - "name": "nginx-templates", - "mountPath": "/etc/nginx/templates" - }, - { - "name": "nginx-tls", - "mountPath": "/etc/ssl/nginx" + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-ui-go" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-nginx" + "hostname": "edgex-ui-go" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59900", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-device-virtual" } }, "spec": { @@ -7460,33 +7261,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/support-notifications", + "path": "/tmp/edgex/secrets/device-virtual", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-support-notifications", - "image": "edgexfoundry/support-notifications:3.0.0", + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:3.0.0", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-device-virtual" } ], "resources": {}, @@ -7497,44 +7298,44 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-notifications" + "mountPath": "/tmp/edgex/secrets/device-virtual" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-59882", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-command" } }, "spec": { @@ -7546,37 +7347,37 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/app-rules-engine", + "path": "/tmp/edgex/secrets/core-command", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-app-rules-engine", - "image": "edgexfoundry/app-service-configurable:3.0.0", + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:3.0.0", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" + "name": "EXTERNALMQTT_URL", + "value": "tcp://edgex-mqtt-broker:1883" }, { - "name": "EDGEX_PROFILE", - "value": "rules-engine" + "name": "SERVICE_HOST", + "value": "edgex-core-command" } ], "resources": {}, @@ -7587,130 +7388,160 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/app-rules-engine" + "mountPath": "/tmp/edgex/secrets/core-command" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "edgex-core-common-config-bootstrapper", + "name": "edgex-nginx", + "service": { + "ports": [ + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 + } + ], + "selector": { + "app": "edgex-nginx" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-common-config-bootstrapper" + "app": "edgex-nginx" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-common-config-bootstrapper" + "app": "edgex-nginx" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, + { + "name": "tmpfs-volume4", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/core-common-config-bootstrapper", - "type": "DirectoryOrCreate" - } + "name": "nginx-templates", + "emptyDir": {} + }, + { + "name": "nginx-tls", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-common-config-bootstrapper", - "image": "edgexfoundry/core-common-config-bootstrapper:3.0.0", + "name": "edgex-nginx", + "image": "nginx:1.24.0-alpine-slim", + "ports": [ + { + "name": "tcp-8443", + "containerPort": 8443, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], - "env": [ + "resources": {}, + "volumeMounts": [ { - "name": "APP_SERVICES_CLIENTS_CORE_METADATA_HOST", - "value": "edgex-core-metadata" + "name": "tmpfs-volume1", + "mountPath": "/etc/nginx/conf.d" }, { - "name": "ALL_SERVICES_MESSAGEBUS_HOST", - "value": "edgex-redis" + "name": "tmpfs-volume2", + "mountPath": "/var/cache/nginx" }, { - "name": "ALL_SERVICES_DATABASE_HOST", - "value": "edgex-redis" + "name": "tmpfs-volume3", + "mountPath": "/var/log/nginx" }, { - "name": "DEVICE_SERVICES_CLIENTS_CORE_METADATA_HOST", - "value": "edgex-core-metadata" + "name": "tmpfs-volume4", + "mountPath": "/var/run" }, - { - "name": "ALL_SERVICES_REGISTRY_HOST", - "value": "edgex-core-consul" - } - ], - "resources": {}, - "volumeMounts": [ { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-common-config-bootstrapper" + "name": "nginx-templates", + "mountPath": "/etc/nginx/templates" + }, + { + "name": "nginx-tls", + "mountPath": "/etc/ssl/nginx" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-common-config-bootstrapper" + "hostname": "edgex-nginx" } }, "strategy": {} } }, { - "name": "edgex-core-command", - "service": { - "ports": [ - { - "name": "tcp-59882", - "protocol": "TCP", - "port": 59882, - "targetPort": 59882 - } - ], - "selector": { - "app": "edgex-core-command" - } - }, + "name": "edgex-security-secretstore-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-security-secretstore-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-security-secretstore-setup" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} @@ -7718,199 +7549,254 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-command", + "path": "/tmp/edgex/secrets", "type": "DirectoryOrCreate" } + }, + { + "name": "kuiper-sources", + "emptyDir": {} + }, + { + "name": "kuiper-connections", + "emptyDir": {} + }, + { + "name": "vault-config", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-command", - "image": "edgexfoundry/core-command:3.0.0", - "ports": [ - { - "name": "tcp-59882", - "containerPort": 59882, - "protocol": "TCP" - } - ], + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:3.0.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "EXTERNALMQTT_URL", - "value": "tcp://edgex-mqtt-broker:1883" + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" }, { - "name": "SERVICE_HOST", - "value": "edgex-core-command" + "name": "EDGEX_ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],message-bus[device-rest],redisdb[device-virtual],message-bus[device-virtual]" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "EDGEX_ADD_SECRETSTORE_TOKENS" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-command" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets" + }, + { + "name": "kuiper-sources", + "mountPath": "/tmp/kuiper" + }, + { + "name": "kuiper-connections", + "mountPath": "/tmp/kuiper-connections" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-security-secretstore-setup" } }, "strategy": {} } }, { - "name": "edgex-redis", - "service": { - "ports": [ - { - "name": "tcp-6379", - "protocol": "TCP", - "port": 6379, - "targetPort": 6379 - } - ], - "selector": { - "app": "edgex-redis" - } - }, + "name": "edgex-security-proxy-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-security-proxy-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-security-proxy-setup" } }, "spec": { "volumes": [ { - "name": "tmpfs-volume1", + "name": "edgex-init", "emptyDir": {} }, { - "name": "db-data", + "name": "vault-config", "emptyDir": {} }, { - "name": "edgex-init", + "name": "nginx-templates", "emptyDir": {} }, { - "name": "redis-config", + "name": "nginx-tls", "emptyDir": {} }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } + }, + { + "name": "consul-acl-token", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-redis", - "image": "redis:7.0.11-alpine", - "ports": [ - { - "name": "tcp-6379", - "containerPort": 6379, - "protocol": "TCP" - } - ], + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:3.0.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { - "name": "DATABASECONFIG_PATH", - "value": "/run/redis/conf" + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" }, { - "name": "DATABASECONFIG_NAME", - "value": "redis.conf" + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "EDGEX_ADD_PROXY_ROUTE" + }, + { + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" } ], "resources": {}, "volumeMounts": [ { - "name": "tmpfs-volume1", - "mountPath": "/run" + "name": "edgex-init", + "mountPath": "/edgex-init" }, { - "name": "db-data", - "mountPath": "/data" + "name": "vault-config", + "mountPath": "/vault/config" }, { - "name": "edgex-init", - "mountPath": "/edgex-init" + "name": "nginx-templates", + "mountPath": "/etc/nginx/templates" }, { - "name": "redis-config", - "mountPath": "/run/redis/conf" + "name": "nginx-tls", + "mountPath": "/etc/ssl/nginx" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-security-proxy-setup" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-59986", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-device-rest" } }, "spec": { @@ -7922,33 +7808,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/device-virtual", + "path": "/tmp/edgex/secrets/device-rest", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-device-virtual", - "image": "edgexfoundry/device-virtual:3.0.0", + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:3.0.0", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-59986", + "containerPort": 59986, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-minnesota" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "value": "edgex-device-rest" } ], "resources": {}, @@ -7959,147 +7845,44 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-virtual" - } - ], - "imagePullPolicy": "IfNotPresent" - } - ], - "hostname": "edgex-device-virtual" - } - }, - "strategy": {} - } - } - ] - }, - { - "versionName": "ireland", - "configMaps": [ - { - "metadata": { - "name": "common-variable-ireland", - "creationTimestamp": null - }, - "data": { - "API_GATEWAY_HOST": "edgex-kong", - "API_GATEWAY_STATUS_PORT": "8100", - "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", - "CLIENTS_CORE_DATA_HOST": "edgex-core-data", - "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", - "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", - "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", - "DATABASES_PRIMARY_HOST": "edgex-redis", - "EDGEX_SECURITY_SECRET_STORE": "true", - "MESSAGEQUEUE_HOST": "edgex-redis", - "PROXY_SETUP_HOST": "edgex-security-proxy-setup", - "REGISTRY_HOST": "edgex-core-consul", - "SECRETSTORE_HOST": "edgex-vault", - "SECRETSTORE_PORT": "8200", - "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", - "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", - "STAGEGATE_DATABASE_HOST": "edgex-redis", - "STAGEGATE_DATABASE_PORT": "6379", - "STAGEGATE_DATABASE_READYPORT": "6379", - "STAGEGATE_KONGDB_HOST": "edgex-kong-db", - "STAGEGATE_KONGDB_PORT": "5432", - "STAGEGATE_KONGDB_READYPORT": "54325", - "STAGEGATE_READY_TORUNPORT": "54329", - "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", - "STAGEGATE_REGISTRY_PORT": "8500", - "STAGEGATE_REGISTRY_READYPORT": "54324", - "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", - "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", - "STAGEGATE_WAITFOR_TIMEOUT": "60s" - } - } - ], - "components": [ - { - "name": "edgex-security-bootstrapper", - "deployment": { - "selector": { - "matchLabels": { - "app": "edgex-security-bootstrapper" - } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-security-bootstrapper" - } - }, - "spec": { - "volumes": [ - { - "name": "edgex-init", - "emptyDir": {} - } - ], - "containers": [ - { - "name": "edgex-security-bootstrapper", - "image": "edgexfoundry/security-bootstrapper:2.0.0", - "envFrom": [ - { - "configMapRef": { - "name": "common-variable-ireland" - } - } - ], - "env": [ - { - "name": "EDGEX_USER", - "value": "2002" - }, - { - "name": "EDGEX_GROUP", - "value": "2001" - } - ], - "resources": {}, - "volumeMounts": [ - { - "name": "edgex-init", - "mountPath": "/edgex-init" + "mountPath": "/tmp/edgex/secrets/device-rest" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-bootstrapper" + "hostname": "edgex-device-rest" } }, "strategy": {} } }, { - "name": "edgex-vault", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-8200", + "name": "tcp-6379", "protocol": "TCP", - "port": 8200, - "targetPort": 8200 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-vault" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-vault" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-vault" + "app": "edgex-redis" } }, "spec": { @@ -8109,104 +7892,155 @@ "emptyDir": {} }, { - "name": "edgex-init", + "name": "db-data", "emptyDir": {} }, { - "name": "vault-file", + "name": "edgex-init", "emptyDir": {} }, { - "name": "vault-logs", + "name": "redis-config", "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-vault", - "image": "vault:1.7.2", + "name": "edgex-redis", + "image": "redis:7.0.11-alpine", "ports": [ { - "name": "tcp-8200", - "containerPort": 8200, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "VAULT_ADDR", - "value": "http://edgex-vault:8200" - }, - { - "name": "VAULT_CONFIG_DIR", - "value": "/vault/config" + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" }, { - "name": "VAULT_UI", - "value": "true" + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" } ], "resources": {}, "volumeMounts": [ { "name": "tmpfs-volume1", - "mountPath": "/vault/config" + "mountPath": "/run" + }, + { + "name": "db-data", + "mountPath": "/data" }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "vault-file", - "mountPath": "/vault/file" + "name": "redis-config", + "mountPath": "/run/redis/conf" }, { - "name": "vault-logs", - "mountPath": "/vault/logs" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-vault" + "hostname": "edgex-redis" } }, "strategy": {} } - }, + } + ] + }, + { + "versionName": "ireland", + "configMaps": [ { - "name": "edgex-core-command", + "metadata": { + "name": "common-variables", + "creationTimestamp": null + }, + "data": { + "API_GATEWAY_HOST": "edgex-kong", + "API_GATEWAY_STATUS_PORT": "8100", + "CLIENTS_CORE_COMMAND_HOST": "edgex-core-command", + "CLIENTS_CORE_DATA_HOST": "edgex-core-data", + "CLIENTS_CORE_METADATA_HOST": "edgex-core-metadata", + "CLIENTS_SUPPORT_NOTIFICATIONS_HOST": "edgex-support-notifications", + "CLIENTS_SUPPORT_SCHEDULER_HOST": "edgex-support-scheduler", + "DATABASES_PRIMARY_HOST": "edgex-redis", + "EDGEX_SECURITY_SECRET_STORE": "true", + "MESSAGEQUEUE_HOST": "edgex-redis", + "PROXY_SETUP_HOST": "edgex-security-proxy-setup", + "REGISTRY_HOST": "edgex-core-consul", + "SECRETSTORE_HOST": "edgex-vault", + "SECRETSTORE_PORT": "8200", + "STAGEGATE_BOOTSTRAPPER_HOST": "edgex-security-bootstrapper", + "STAGEGATE_BOOTSTRAPPER_STARTPORT": "54321", + "STAGEGATE_DATABASE_HOST": "edgex-redis", + "STAGEGATE_DATABASE_PORT": "6379", + "STAGEGATE_DATABASE_READYPORT": "6379", + "STAGEGATE_KONGDB_HOST": "edgex-kong-db", + "STAGEGATE_KONGDB_PORT": "5432", + "STAGEGATE_KONGDB_READYPORT": "54325", + "STAGEGATE_READY_TORUNPORT": "54329", + "STAGEGATE_REGISTRY_HOST": "edgex-core-consul", + "STAGEGATE_REGISTRY_PORT": "8500", + "STAGEGATE_REGISTRY_READYPORT": "54324", + "STAGEGATE_SECRETSTORESETUP_HOST": "edgex-security-secretstore-setup", + "STAGEGATE_SECRETSTORESETUP_TOKENS_READYPORT": "54322", + "STAGEGATE_WAITFOR_TIMEOUT": "60s" + } + } + ], + "components": [ + { + "name": "edgex-app-rules-engine", "service": { "ports": [ { - "name": "tcp-59882", + "name": "tcp-59701", "protocol": "TCP", - "port": 59882, - "targetPort": 59882 + "port": 59701, + "targetPort": 59701 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-app-rules-engine" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-app-rules-engine" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-app-rules-engine" } }, "spec": { @@ -8218,33 +8052,45 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-command", + "path": "/tmp/edgex/secrets/app-rules-engine", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-command", - "image": "edgexfoundry/core-command:2.0.0", + "name": "edgex-app-rules-engine", + "image": "edgexfoundry/app-service-configurable:2.0.1", "ports": [ { - "name": "tcp-59882", - "containerPort": 59882, + "name": "tcp-59701", + "containerPort": 59701, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ + { + "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-redis" + }, + { + "name": "EDGEX_PROFILE", + "value": "rules-engine" + }, { "name": "SERVICE_HOST", - "value": "edgex-core-command" + "value": "edgex-app-rules-engine" } ], "resources": {}, @@ -8255,44 +8101,44 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-command" + "mountPath": "/tmp/edgex/secrets/app-rules-engine" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-app-rules-engine" } }, "strategy": {} } }, { - "name": "edgex-app-rules-engine", + "name": "edgex-core-command", "service": { "ports": [ { - "name": "tcp-59701", + "name": "tcp-59882", "protocol": "TCP", - "port": 59701, - "targetPort": 59701 + "port": 59882, + "targetPort": 59882 } ], "selector": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-command" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-rules-engine" + "app": "edgex-core-command" } }, "spec": { @@ -8304,45 +8150,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/app-rules-engine", + "path": "/tmp/edgex/secrets/core-command", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-app-rules-engine", - "image": "edgexfoundry/app-service-configurable:2.0.1", + "name": "edgex-core-command", + "image": "edgexfoundry/core-command:2.0.0", "ports": [ { - "name": "tcp-59701", - "containerPort": 59701, + "name": "tcp-59882", + "containerPort": 59882, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ - { - "name": "EDGEX_PROFILE", - "value": "rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_PUBLISHHOST_HOST", - "value": "edgex-redis" - }, { "name": "SERVICE_HOST", - "value": "edgex-app-rules-engine" - }, - { - "name": "TRIGGER_EDGEXMESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-redis" + "value": "edgex-core-command" } ], "resources": {}, @@ -8353,44 +8187,31 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/app-rules-engine" + "mountPath": "/tmp/edgex/secrets/core-command" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-rules-engine" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "edgex-device-rest", - "service": { - "ports": [ - { - "name": "tcp-59986", - "protocol": "TCP", - "port": 59986, - "targetPort": 59986 - } - ], - "selector": { - "app": "edgex-device-rest" - } - }, + "name": "edgex-security-proxy-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-security-proxy-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-security-proxy-setup" } }, "spec": { @@ -8399,36 +8220,72 @@ "name": "edgex-init", "emptyDir": {} }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/device-rest", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-device-rest", - "image": "edgexfoundry/device-rest:2.0.0", - "ports": [ - { - "name": "tcp-59986", - "containerPort": 59986, - "protocol": "TCP" - } - ], + "name": "edgex-security-proxy-setup", + "image": "edgexfoundry/security-proxy-setup:2.0.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "name": "KONGURL_SERVER", + "value": "edgex-kong" + }, + { + "name": "ADD_PROXY_ROUTE" + }, + { + "name": "ROUTES_CORE_METADATA_HOST", + "value": "edgex-core-metadata" + }, + { + "name": "ROUTES_CORE_COMMAND_HOST", + "value": "edgex-core-command" + }, + { + "name": "ROUTES_CORE_CONSUL_HOST", + "value": "edgex-core-consul" + }, + { + "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", + "value": "edgex-support-notifications" + }, + { + "name": "ROUTES_SYS_MGMT_AGENT_HOST", + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "ROUTES_RULES_ENGINE_HOST", + "value": "edgex-kuiper" + }, + { + "name": "ROUTES_DEVICE_VIRTUAL_HOST", + "value": "device-virtual" + }, + { + "name": "ROUTES_SUPPORT_SCHEDULER_HOST", + "value": "edgex-support-scheduler" + }, + { + "name": "ROUTES_CORE_DATA_HOST", + "value": "edgex-core-data" } ], "resources": {}, @@ -8437,101 +8294,144 @@ "name": "edgex-init", "mountPath": "/edgex-init" }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-rest" + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-security-proxy-setup" } }, "strategy": {} } }, { - "name": "edgex-device-virtual", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-59900", + "name": "tcp-8500", "protocol": "TCP", - "port": 59900, - "targetPort": 59900 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-device-virtual" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-virtual" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-virtual" + "app": "edgex-core-consul" } }, "spec": { "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, + { + "name": "consul-acl-token", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/device-virtual", + "path": "/tmp/edgex/secrets/edgex-consul", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-device-virtual", - "image": "edgexfoundry/device-virtual:2.0.0", + "name": "edgex-core-consul", + "image": "consul:1.9.5", "ports": [ { - "name": "tcp-59900", - "containerPort": 59900, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-device-virtual" + "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", + "value": "/consul/config/consul_acl_done" + }, + { + "name": "EDGEX_GROUP", + "value": "2001" + }, + { + "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", + "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + }, + { + "name": "EDGEX_USER", + "value": "2002" + }, + { + "name": "ADD_REGISTRY_ACL_ROLES" } ], "resources": {}, "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, + { + "name": "consul-acl-token", + "mountPath": "/tmp/edgex/secrets/consul-acl-token" + }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/device-virtual" + "mountPath": "/tmp/edgex/secrets/edgex-consul" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-virtual" + "hostname": "edgex-core-consul" } }, "strategy": {} @@ -8594,38 +8494,38 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-redis" - }, { "name": "EDGEX__DEFAULT__PORT", "value": "6379" }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "rules-events" - }, { "name": "EDGEX__DEFAULT__PROTOCOL", "value": "redis" }, - { - "name": "KUIPER__BASIC__RESTPORT", - "value": "59720" - }, { "name": "EDGEX__DEFAULT__TYPE", "value": "redis" }, + { + "name": "EDGEX__DEFAULT__TOPIC", + "value": "rules-events" + }, { "name": "KUIPER__BASIC__CONSOLELOG", "value": "true" + }, + { + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-redis" + }, + { + "name": "KUIPER__BASIC__RESTPORT", + "value": "59720" } ], "resources": {}, @@ -8653,30 +8553,35 @@ } }, { - "name": "edgex-security-secretstore-setup", + "name": "edgex-sys-mgmt-agent", + "service": { + "ports": [ + { + "name": "tcp-58890", + "protocol": "TCP", + "port": 58890, + "targetPort": 58890 + } + ], + "selector": { + "app": "edgex-sys-mgmt-agent" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-secretstore-setup" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-secretstore-setup" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} @@ -8684,121 +8589,190 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets", + "path": "/tmp/edgex/secrets/sys-mgmt-agent", "type": "DirectoryOrCreate" } - }, - { - "name": "kong", - "emptyDir": {} - }, - { - "name": "kuiper-config", - "emptyDir": {} - }, - { - "name": "vault-config", - "emptyDir": {} } ], "containers": [ { - "name": "edgex-security-secretstore-setup", - "image": "edgexfoundry/security-secretstore-setup:2.0.0", + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/sys-mgmt-agent:2.0.0", + "ports": [ + { + "name": "tcp-58890", + "containerPort": 58890, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "ADD_KNOWN_SECRETS", - "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" }, { - "name": "EDGEX_USER", - "value": "2002" + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "SERVICE_HOST", + "value": "edgex-sys-mgmt-agent" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-sys-mgmt-agent" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-core-data", + "service": { + "ports": [ + { + "name": "tcp-5563", + "protocol": "TCP", + "port": 5563, + "targetPort": 5563 + }, + { + "name": "tcp-59880", + "protocol": "TCP", + "port": 59880, + "targetPort": 59880 + } + ], + "selector": { + "app": "edgex-core-data" + } + }, + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-core-data" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-core-data" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/core-data", + "type": "DirectoryOrCreate" + } + } + ], + "containers": [ + { + "name": "edgex-core-data", + "image": "edgexfoundry/core-data:2.0.0", + "ports": [ + { + "name": "tcp-5563", + "containerPort": 5563, + "protocol": "TCP" }, { - "name": "EDGEX_GROUP", - "value": "2001" - }, + "name": "tcp-59880", + "containerPort": 59880, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variables" + } + } + ], + "env": [ { - "name": "ADD_SECRETSTORE_TOKENS" + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/core-data/secrets-token.json" }, { - "name": "SECUREMESSAGEBUS_TYPE", - "value": "redis" + "name": "SERVICE_HOST", + "value": "edgex-core-data" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/vault" - }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets" - }, - { - "name": "kong", - "mountPath": "/tmp/kong" - }, - { - "name": "kuiper-config", - "mountPath": "/tmp/kuiper" - }, - { - "name": "vault-config", - "mountPath": "/vault/config" + "mountPath": "/tmp/edgex/secrets/core-data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-secretstore-setup" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-kong-db", + "name": "edgex-vault", "service": { "ports": [ { - "name": "tcp-5432", + "name": "tcp-8200", "protocol": "TCP", - "port": 5432, - "targetPort": 5432 + "port": 8200, + "targetPort": 8200 } ], "selector": { - "app": "edgex-kong-db" + "app": "edgex-vault" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kong-db" + "app": "edgex-vault" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kong-db" + "app": "edgex-vault" } }, "spec": { @@ -8807,347 +8781,408 @@ "name": "tmpfs-volume1", "emptyDir": {} }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "tmpfs-volume3", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "postgres-config", + "name": "vault-file", "emptyDir": {} }, { - "name": "postgres-data", + "name": "vault-logs", "emptyDir": {} } ], "containers": [ { - "name": "edgex-kong-db", - "image": "postgres:12.3-alpine", + "name": "edgex-vault", + "image": "vault:1.7.2", "ports": [ { - "name": "tcp-5432", - "containerPort": 5432, + "name": "tcp-8200", + "containerPort": 8200, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "POSTGRES_USER", - "value": "kong" + "name": "VAULT_UI", + "value": "true" }, { - "name": "POSTGRES_DB", - "value": "kong" + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" }, { - "name": "POSTGRES_PASSWORD_FILE", - "value": "/tmp/postgres-config/.pgpassword" + "name": "VAULT_ADDR", + "value": "http://edgex-vault:8200" } ], "resources": {}, "volumeMounts": [ { "name": "tmpfs-volume1", - "mountPath": "/var/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/tmp" - }, - { - "name": "tmpfs-volume3", - "mountPath": "/run" + "mountPath": "/vault/config" }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "postgres-config", - "mountPath": "/tmp/postgres-config" + "name": "vault-file", + "mountPath": "/vault/file" }, { - "name": "postgres-data", - "mountPath": "/var/lib/postgresql/data" + "name": "vault-logs", + "mountPath": "/vault/logs" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kong-db" + "hostname": "edgex-vault" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "edgex-kong", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-8000", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8100", + "protocol": "TCP", + "port": 8100, + "targetPort": 8100 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 } ], "selector": { - "app": "edgex-core-consul" + "app": "edgex-kong" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "edgex-kong" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "edgex-kong" } }, "spec": { "volumes": [ { - "name": "consul-config", + "name": "tmpfs-volume1", "emptyDir": {} }, { - "name": "consul-data", + "name": "tmpfs-volume2", "emptyDir": {} }, { "name": "edgex-init", "emptyDir": {} }, - { - "name": "consul-acl-token", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/edgex-consul", + "path": "/tmp/edgex/secrets/security-proxy-setup", "type": "DirectoryOrCreate" } + }, + { + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "kong", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-consul", - "image": "consul:1.9.5", + "name": "edgex-kong", + "image": "kong:2.4.1-alpine", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8100", + "containerPort": 8100, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "STAGEGATE_REGISTRY_ACL_SENTINELFILEPATH", - "value": "/consul/config/consul_acl_done" + "name": "KONG_PG_HOST", + "value": "edgex-kong-db" }, { - "name": "ADD_REGISTRY_ACL_ROLES" + "name": "KONG_DNS_ORDER", + "value": "LAST,A,CNAME" }, { - "name": "STAGEGATE_REGISTRY_ACL_BOOTSTRAPTOKENPATH", - "value": "/tmp/edgex/secrets/consul-acl-token/bootstrap_token.json" + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" }, { - "name": "EDGEX_USER", - "value": "2002" + "name": "KONG_DNS_VALID_TTL", + "value": "1" }, { - "name": "EDGEX_GROUP", - "value": "2001" + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_PG_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + }, + { + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" + }, + { + "name": "KONG_STATUS_LISTEN", + "value": "0.0.0.0:8100" + }, + { + "name": "KONG_ADMIN_LISTEN", + "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" } ], "resources": {}, "volumeMounts": [ { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "tmpfs-volume1", + "mountPath": "/run" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "tmpfs-volume2", + "mountPath": "/tmp" }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/security-proxy-setup" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/edgex-consul" + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "edgex-kong" } }, "strategy": {} } }, { - "name": "edgex-core-data", + "name": "edgex-kong-db", "service": { "ports": [ { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-59880", + "name": "tcp-5432", "protocol": "TCP", - "port": 59880, - "targetPort": 59880 + "port": 5432, + "targetPort": 5432 } ], "selector": { - "app": "edgex-core-data" + "app": "edgex-kong-db" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-kong-db" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-kong-db" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "tmpfs-volume3", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/core-data", - "type": "DirectoryOrCreate" - } + "name": "postgres-config", + "emptyDir": {} + }, + { + "name": "postgres-data", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-data", - "image": "edgexfoundry/core-data:2.0.0", + "name": "edgex-kong-db", + "image": "postgres:12.3-alpine", "ports": [ { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-59880", - "containerPort": 59880, + "name": "tcp-5432", + "containerPort": 5432, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-data" + "name": "POSTGRES_DB", + "value": "kong" }, { - "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/core-data/secrets-token.json" + "name": "POSTGRES_PASSWORD_FILE", + "value": "/tmp/postgres-config/.pgpassword" + }, + { + "name": "POSTGRES_USER", + "value": "kong" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/var/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" + }, + { + "name": "tmpfs-volume3", + "mountPath": "/run" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-data" + "name": "postgres-config", + "mountPath": "/tmp/postgres-config" + }, + { + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-kong-db" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-59860", + "name": "tcp-59881", "protocol": "TCP", - "port": 59860, - "targetPort": 59860 + "port": 59881, + "targetPort": 59881 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "spec": { @@ -9159,33 +9194,37 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/support-notifications", + "path": "/tmp/edgex/secrets/core-metadata", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-support-notifications", - "image": "edgexfoundry/support-notifications:2.0.0", + "name": "edgex-core-metadata", + "image": "edgexfoundry/core-metadata:2.0.0", "ports": [ { - "name": "tcp-59860", - "containerPort": 59860, + "name": "tcp-59881", + "containerPort": 59881, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" + }, { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-core-metadata" } ], "resources": {}, @@ -9196,283 +9235,239 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-notifications" + "mountPath": "/tmp/edgex/secrets/core-metadata" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "edgex-security-proxy-setup", + "name": "edgex-redis", + "service": { + "ports": [ + { + "name": "tcp-6379", + "protocol": "TCP", + "port": 6379, + "targetPort": 6379 + } + ], + "selector": { + "app": "edgex-redis" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-proxy-setup" + "app": "edgex-redis" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "db-data", + "emptyDir": {} + }, { "name": "edgex-init", "emptyDir": {} }, { - "name": "consul-acl-token", + "name": "redis-config", "emptyDir": {} }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", + "path": "/tmp/edgex/secrets/security-bootstrapper-redis", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-security-proxy-setup", - "image": "edgexfoundry/security-proxy-setup:2.0.0", + "name": "edgex-redis", + "image": "redis:6.2.4-alpine", + "ports": [ + { + "name": "tcp-6379", + "containerPort": 6379, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "ROUTES_CORE_CONSUL_HOST", - "value": "edgex-core-consul" - }, - { - "name": "ROUTES_RULES_ENGINE_HOST", - "value": "edgex-kuiper" - }, - { - "name": "ROUTES_DEVICE_VIRTUAL_HOST", - "value": "device-virtual" - }, - { - "name": "ROUTES_CORE_METADATA_HOST", - "value": "edgex-core-metadata" - }, - { - "name": "ROUTES_SUPPORT_SCHEDULER_HOST", - "value": "edgex-support-scheduler" - }, - { - "name": "ADD_PROXY_ROUTE" - }, - { - "name": "ROUTES_SUPPORT_NOTIFICATIONS_HOST", - "value": "edgex-support-notifications" - }, - { - "name": "ROUTES_CORE_DATA_HOST", - "value": "edgex-core-data" - }, - { - "name": "ROUTES_SYS_MGMT_AGENT_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "ROUTES_CORE_COMMAND_HOST", - "value": "edgex-core-command" + "name": "DATABASECONFIG_PATH", + "value": "/run/redis/conf" }, { - "name": "KONGURL_SERVER", - "value": "edgex-kong" + "name": "DATABASECONFIG_NAME", + "value": "redis.conf" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "db-data", + "mountPath": "/data" + }, { "name": "edgex-init", "mountPath": "/edgex-init" }, { - "name": "consul-acl-token", - "mountPath": "/tmp/edgex/secrets/consul-acl-token" + "name": "redis-config", + "mountPath": "/run/redis/conf" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-proxy-setup" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-redis", + "name": "edgex-support-scheduler", "service": { "ports": [ { - "name": "tcp-6379", + "name": "tcp-59861", "protocol": "TCP", - "port": 6379, - "targetPort": 6379 + "port": 59861, + "targetPort": 59861 } ], "selector": { - "app": "edgex-redis" + "app": "edgex-support-scheduler" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "edgex-support-scheduler" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "edgex-support-scheduler" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "db-data", - "emptyDir": {} - }, { "name": "edgex-init", "emptyDir": {} }, - { - "name": "redis-config", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-bootstrapper-redis", + "path": "/tmp/edgex/secrets/support-scheduler", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-redis", - "image": "redis:6.2.4-alpine", + "name": "edgex-support-scheduler", + "image": "edgexfoundry/support-scheduler:2.0.0", "ports": [ { - "name": "tcp-6379", - "containerPort": 6379, + "name": "tcp-59861", + "containerPort": 59861, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "DATABASECONFIG_PATH", - "value": "/run/redis/conf" - }, - { - "name": "DATABASECONFIG_NAME", - "value": "redis.conf" - } - ], - "resources": {}, - "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" }, { - "name": "db-data", - "mountPath": "/data" + "name": "SERVICE_HOST", + "value": "edgex-support-scheduler" }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + } + ], + "resources": {}, + "volumeMounts": [ { "name": "edgex-init", "mountPath": "/edgex-init" }, - { - "name": "redis-config", - "mountPath": "/run/redis/conf" - }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-bootstrapper-redis" + "mountPath": "/tmp/edgex/secrets/support-scheduler" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-redis" + "hostname": "edgex-support-scheduler" } }, "strategy": {} } }, { - "name": "edgex-kong", - "service": { - "ports": [ - { - "name": "tcp-8000", - "protocol": "TCP", - "port": 8000, - "targetPort": 8000 - }, - { - "name": "tcp-8100", - "protocol": "TCP", - "port": 8100, - "targetPort": 8100 - }, - { - "name": "tcp-8443", - "protocol": "TCP", - "port": 8443, - "targetPort": 8443 - } - ], - "selector": { - "app": "edgex-kong" - } - }, + "name": "edgex-security-secretstore-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-kong" + "app": "edgex-security-secretstore-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kong" + "app": "edgex-security-secretstore-setup" } }, "spec": { @@ -9492,91 +9487,53 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/security-proxy-setup", + "path": "/tmp/edgex/secrets", "type": "DirectoryOrCreate" } }, { - "name": "postgres-config", + "name": "kong", "emptyDir": {} }, { - "name": "kong", + "name": "kuiper-config", + "emptyDir": {} + }, + { + "name": "vault-config", "emptyDir": {} } ], "containers": [ { - "name": "edgex-kong", - "image": "kong:2.4.1-alpine", - "ports": [ - { - "name": "tcp-8000", - "containerPort": 8000, - "protocol": "TCP" - }, - { - "name": "tcp-8100", - "containerPort": 8100, - "protocol": "TCP" - }, - { - "name": "tcp-8443", - "containerPort": 8443, - "protocol": "TCP" - } - ], + "name": "edgex-security-secretstore-setup", + "image": "edgexfoundry/security-secretstore-setup:2.0.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "KONG_DNS_ORDER", - "value": "LAST,A,CNAME" - }, - { - "name": "KONG_PG_PASSWORD_FILE", - "value": "/tmp/postgres-config/.pgpassword" - }, - { - "name": "KONG_ADMIN_LISTEN", - "value": "127.0.0.1:8001, 127.0.0.1:8444 ssl" - }, - { - "name": "KONG_PROXY_ACCESS_LOG", - "value": "/dev/stdout" - }, - { - "name": "KONG_STATUS_LISTEN", - "value": "0.0.0.0:8100" - }, - { - "name": "KONG_DATABASE", - "value": "postgres" - }, - { - "name": "KONG_DNS_VALID_TTL", - "value": "1" + "name": "ADD_SECRETSTORE_TOKENS" }, { - "name": "KONG_PG_HOST", - "value": "edgex-kong-db" + "name": "ADD_KNOWN_SECRETS", + "value": "redisdb[app-rules-engine],redisdb[device-rest],redisdb[device-virtual]" }, { - "name": "KONG_ADMIN_ERROR_LOG", - "value": "/dev/stderr" + "name": "EDGEX_GROUP", + "value": "2001" }, { - "name": "KONG_ADMIN_ACCESS_LOG", - "value": "/dev/stdout" + "name": "SECUREMESSAGEBUS_TYPE", + "value": "redis" }, { - "name": "KONG_PROXY_ERROR_LOG", - "value": "/dev/stderr" + "name": "EDGEX_USER", + "value": "2002" } ], "resources": {}, @@ -9587,7 +9544,7 @@ }, { "name": "tmpfs-volume2", - "mountPath": "/tmp" + "mountPath": "/vault" }, { "name": "edgex-init", @@ -9595,52 +9552,56 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/security-proxy-setup" + "mountPath": "/tmp/edgex/secrets" }, { - "name": "postgres-config", - "mountPath": "/tmp/postgres-config" + "name": "kong", + "mountPath": "/tmp/kong" }, { - "name": "kong", - "mountPath": "/usr/local/kong" + "name": "kuiper-config", + "mountPath": "/tmp/kuiper" + }, + { + "name": "vault-config", + "mountPath": "/vault/config" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-kong" + "hostname": "edgex-security-secretstore-setup" } }, "strategy": {} } }, { - "name": "edgex-support-scheduler", + "name": "edgex-device-virtual", "service": { "ports": [ { - "name": "tcp-59861", + "name": "tcp-59900", "protocol": "TCP", - "port": 59861, - "targetPort": 59861 + "port": 59900, + "targetPort": 59900 } ], "selector": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-scheduler" + "app": "edgex-device-virtual" } }, "spec": { @@ -9652,41 +9613,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/support-scheduler", + "path": "/tmp/edgex/secrets/device-virtual", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-support-scheduler", - "image": "edgexfoundry/support-scheduler:2.0.0", + "name": "edgex-device-virtual", + "image": "edgexfoundry/device-virtual:2.0.0", "ports": [ { - "name": "tcp-59861", - "containerPort": 59861, + "name": "tcp-59900", + "containerPort": 59900, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" - }, { "name": "SERVICE_HOST", - "value": "edgex-support-scheduler" + "value": "edgex-device-virtual" } ], "resources": {}, @@ -9697,44 +9650,44 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/support-scheduler" + "mountPath": "/tmp/edgex/secrets/device-virtual" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-scheduler" + "hostname": "edgex-device-virtual" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-device-rest", "service": { "ports": [ { - "name": "tcp-58890", + "name": "tcp-59986", "protocol": "TCP", - "port": 58890, - "targetPort": 58890 + "port": 59986, + "targetPort": 59986 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-device-rest" } }, "spec": { @@ -9746,41 +9699,96 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/sys-mgmt-agent", + "path": "/tmp/edgex/secrets/device-rest", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "edgexfoundry/sys-mgmt-agent:2.0.0", - "ports": [ - { - "name": "tcp-58890", - "containerPort": 58890, - "protocol": "TCP" - } - ], + "name": "edgex-device-rest", + "image": "edgexfoundry/device-rest:2.0.0", + "ports": [ + { + "name": "tcp-59986", + "containerPort": 59986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variables" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "edgex-init", + "mountPath": "/edgex-init" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/device-rest" + } + ], + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-security-bootstrapper", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-security-bootstrapper" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-security-bootstrapper" + } + }, + "spec": { + "volumes": [ + { + "name": "edgex-init", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "edgex-security-bootstrapper", + "image": "edgexfoundry/security-bootstrapper:2.0.0", "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" - }, - { - "name": "METRICSMECHANISM", - "value": "executor" + "name": "EDGEX_USER", + "value": "2002" }, { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "EDGEX_GROUP", + "value": "2001" } ], "resources": {}, @@ -9788,47 +9796,43 @@ { "name": "edgex-init", "mountPath": "/edgex-init" - }, - { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/sys-mgmt-agent" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-security-bootstrapper" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-59881", + "name": "tcp-59860", "protocol": "TCP", - "port": 59881, - "targetPort": 59881 + "port": 59860, + "targetPort": 59860 } ], "selector": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-support-notifications" } }, "spec": { @@ -9840,37 +9844,33 @@ { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/core-metadata", + "path": "/tmp/edgex/secrets/support-notifications", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-metadata", - "image": "edgexfoundry/core-metadata:2.0.0", + "name": "edgex-support-notifications", + "image": "edgexfoundry/support-notifications:2.0.0", "ports": [ { - "name": "tcp-59881", - "containerPort": 59881, + "name": "tcp-59860", + "containerPort": 59860, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-ireland" + "name": "common-variables" } } ], "env": [ - { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" - }, { "name": "SERVICE_HOST", - "value": "edgex-core-metadata" + "value": "edgex-support-notifications" } ], "resources": {}, @@ -9881,13 +9881,13 @@ }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/core-metadata" + "mountPath": "/tmp/edgex/secrets/support-notifications" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-support-notifications" } }, "strategy": {} @@ -9900,7 +9900,7 @@ "configMaps": [ { "metadata": { - "name": "common-variable-hanoi", + "name": "common-variables", "creationTimestamp": null }, "data": { @@ -9925,305 +9925,257 @@ ], "components": [ { - "name": "edgex-kuiper", + "name": "edgex-core-data", "service": { "ports": [ { - "name": "tcp-20498", + "name": "tcp-5563", "protocol": "TCP", - "port": 20498, - "targetPort": 20498 + "port": 5563, + "targetPort": 5563 }, { - "name": "tcp-48075", + "name": "tcp-48080", "protocol": "TCP", - "port": 48075, - "targetPort": 48075 + "port": 48080, + "targetPort": 48080 } ], "selector": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-kuiper" + "app": "edgex-core-data" } }, "spec": { + "volumes": [ + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-core-data", + "type": "DirectoryOrCreate" + } + } + ], "containers": [ { - "name": "edgex-kuiper", - "image": "emqx/kuiper:1.1.1-alpine", + "name": "edgex-core-data", + "image": "edgexfoundry/docker-core-data-go:1.3.1", "ports": [ { - "name": "tcp-20498", - "containerPort": 20498, + "name": "tcp-5563", + "containerPort": 5563, "protocol": "TCP" }, { - "name": "tcp-48075", - "containerPort": 48075, + "name": "tcp-48080", + "containerPort": 48080, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX__DEFAULT__PORT", - "value": "5566" - }, - { - "name": "EDGEX__DEFAULT__PROTOCOL", - "value": "tcp" - }, - { - "name": "EDGEX__DEFAULT__SERVER", - "value": "edgex-app-service-configurable-rules" - }, - { - "name": "EDGEX__DEFAULT__SERVICESERVER", - "value": "http://edgex-core-data:48080" - }, - { - "name": "EDGEX__DEFAULT__TOPIC", - "value": "events" - }, - { - "name": "KUIPER__BASIC__CONSOLELOG", - "value": "true" + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-core-data/secrets-token.json" }, { - "name": "KUIPER__BASIC__RESTPORT", - "value": "48075" - } - ], - "resources": {}, - "imagePullPolicy": "IfNotPresent" - } - ], - "hostname": "edgex-kuiper" - } - }, - "strategy": {} - } - }, - { - "name": "edgex-secrets-setup", - "deployment": { - "selector": { - "matchLabels": { - "app": "edgex-secrets-setup" - } - }, - "template": { - "metadata": { - "creationTimestamp": null, - "labels": { - "app": "edgex-secrets-setup" - } - }, - "spec": { - "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "secrets-setup-cache", - "emptyDir": {} - }, - { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets", - "type": "DirectoryOrCreate" - } - }, - { - "name": "vault-init", - "emptyDir": {} - } - ], - "containers": [ - { - "name": "edgex-secrets-setup", - "image": "edgexfoundry/docker-security-secrets-setup-go:1.3.1", - "envFrom": [ - { - "configMapRef": { - "name": "common-variable-hanoi" - } + "name": "SERVICE_HOST", + "value": "edgex-core-data" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/tmp" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/run" - }, - { - "name": "secrets-setup-cache", - "mountPath": "/etc/edgex/pki" - }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets" + "mountPath": "/tmp/edgex/secrets/ca" }, { - "name": "vault-init", - "mountPath": "/vault/init" + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-core-data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-secrets-setup" + "hostname": "edgex-core-data" } }, "strategy": {} } }, { - "name": "edgex-proxy", + "name": "edgex-vault", + "service": { + "ports": [ + { + "name": "tcp-8200", + "protocol": "TCP", + "port": 8200, + "targetPort": 8200 + } + ], + "selector": { + "app": "edgex-vault" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-proxy" + "app": "edgex-vault" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-proxy" + "app": "edgex-vault" } }, "spec": { "volumes": [ { - "name": "consul-scripts", + "name": "tmpfs-volume1", "emptyDir": {} }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/ca", + "path": "/tmp/edgex/secrets/edgex-vault", "type": "DirectoryOrCreate" } }, { - "name": "anonymous-volume2", - "hostPath": { - "path": "/tmp/edgex/secrets/edgex-security-proxy-setup", - "type": "DirectoryOrCreate" - } + "name": "vault-file", + "emptyDir": {} + }, + { + "name": "vault-init", + "emptyDir": {} + }, + { + "name": "vault-logs", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-proxy", - "image": "edgexfoundry/docker-security-proxy-setup-go:1.3.1", + "name": "edgex-vault", + "image": "vault:1.5.3", + "ports": [ + { + "name": "tcp-8200", + "containerPort": 8200, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "SECRETSERVICE_SNIS", - "value": "edgex-kong" - }, - { - "name": "SECRETSERVICE_TOKENPATH", - "value": "/tmp/edgex/secrets/edgex-security-proxy-setup/secrets-token.json" - }, - { - "name": "KONGURL_SERVER", - "value": "kong" + "name": "VAULT_CONFIG_DIR", + "value": "/vault/config" }, { - "name": "SECRETSERVICE_CACERTPATH", - "value": "/tmp/edgex/secrets/ca/ca.pem" + "name": "VAULT_UI", + "value": "true" }, { - "name": "SECRETSERVICE_SERVER", - "value": "edgex-vault" + "name": "VAULT_ADDR", + "value": "https://edgex-vault:8200" } ], "resources": {}, "volumeMounts": [ { - "name": "consul-scripts", - "mountPath": "/consul/scripts" + "name": "tmpfs-volume1", + "mountPath": "/vault/config" }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/ca" + "mountPath": "/tmp/edgex/secrets/edgex-vault" }, { - "name": "anonymous-volume2", - "mountPath": "/tmp/edgex/secrets/edgex-security-proxy-setup" + "name": "vault-file", + "mountPath": "/vault/file" + }, + { + "name": "vault-init", + "mountPath": "/vault/init" + }, + { + "name": "vault-logs", + "mountPath": "/vault/logs" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-proxy" + "hostname": "edgex-vault" } }, "strategy": {} } }, { - "name": "edgex-security-bootstrap-database", + "name": "edgex-core-command", + "service": { + "ports": [ + { + "name": "tcp-48082", + "protocol": "TCP", + "port": 48082, + "targetPort": 48082 + } + ], + "selector": { + "app": "edgex-core-command" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-security-bootstrap-database" + "app": "edgex-core-command" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-security-bootstrap-database" + "app": "edgex-core-command" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { @@ -10234,501 +10186,415 @@ { "name": "anonymous-volume2", "hostPath": { - "path": "/tmp/edgex/secrets/edgex-security-bootstrap-redis", + "path": "/tmp/edgex/secrets/edgex-core-command", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-security-bootstrap-database", - "image": "edgexfoundry/docker-security-bootstrap-redis-go:1.3.1", + "name": "edgex-core-command", + "image": "edgexfoundry/docker-core-command-go:1.3.1", + "ports": [ + { + "name": "tcp-48082", + "containerPort": 48082, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/edgex-security-bootstrap-redis/secrets-token.json" + "name": "SERVICE_HOST", + "value": "edgex-core-command" }, { - "name": "SERVICE_HOST", - "value": "edgex-security-bootstrap-database" + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-core-command/secrets-token.json" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/vault" - }, { "name": "anonymous-volume1", "mountPath": "/tmp/edgex/secrets/ca" }, { "name": "anonymous-volume2", - "mountPath": "/tmp/edgex/secrets/edgex-security-bootstrap-redis" + "mountPath": "/tmp/edgex/secrets/edgex-core-command" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-security-bootstrap-database" + "hostname": "edgex-core-command" } }, "strategy": {} } }, { - "name": "kong", + "name": "edgex-app-service-configurable-rules", "service": { "ports": [ { - "name": "tcp-8000", - "protocol": "TCP", - "port": 8000, - "targetPort": 8000 - }, - { - "name": "tcp-8001", - "protocol": "TCP", - "port": 8001, - "targetPort": 8001 - }, - { - "name": "tcp-8443", - "protocol": "TCP", - "port": 8443, - "targetPort": 8443 - }, - { - "name": "tcp-8444", + "name": "tcp-48100", "protocol": "TCP", - "port": 8444, - "targetPort": 8444 + "port": 48100, + "targetPort": 48100 } ], "selector": { - "app": "kong" + "app": "edgex-app-service-configurable-rules" } }, "deployment": { "selector": { "matchLabels": { - "app": "kong" + "app": "edgex-app-service-configurable-rules" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "kong" + "app": "edgex-app-service-configurable-rules" } }, "spec": { - "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "consul-scripts", - "emptyDir": {} - }, - { - "name": "kong", - "emptyDir": {} - } - ], "containers": [ { - "name": "kong", - "image": "kong:2.0.5", + "name": "edgex-app-service-configurable-rules", + "image": "edgexfoundry/docker-app-service-configurable:1.3.1", "ports": [ { - "name": "tcp-8000", - "containerPort": 8000, - "protocol": "TCP" - }, - { - "name": "tcp-8001", - "containerPort": 8001, - "protocol": "TCP" - }, - { - "name": "tcp-8443", - "containerPort": 8443, - "protocol": "TCP" - }, - { - "name": "tcp-8444", - "containerPort": 8444, + "name": "tcp-48100", + "containerPort": 48100, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "KONG_DATABASE", - "value": "postgres" - }, - { - "name": "KONG_PG_HOST", - "value": "kong-db" - }, - { - "name": "KONG_PG_PASSWORD", - "value": "kong" - }, - { - "name": "KONG_PROXY_ACCESS_LOG", - "value": "/dev/stdout" + "name": "EDGEX_PROFILE", + "value": "rules-engine" }, { - "name": "KONG_PROXY_ERROR_LOG", - "value": "/dev/stderr" + "name": "MESSAGEBUS_SUBSCRIBEHOST_HOST", + "value": "edgex-core-data" }, { - "name": "KONG_ADMIN_ACCESS_LOG", - "value": "/dev/stdout" + "name": "SERVICE_PORT", + "value": "48100" }, { - "name": "KONG_ADMIN_ERROR_LOG", - "value": "/dev/stderr" + "name": "SERVICE_HOST", + "value": "edgex-app-service-configurable-rules" }, { - "name": "KONG_ADMIN_LISTEN", - "value": "0.0.0.0:8001, 0.0.0.0:8444 ssl" + "name": "BINDING_PUBLISHTOPIC", + "value": "events" } ], "resources": {}, - "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/tmp" - }, - { - "name": "consul-scripts", - "mountPath": "/consul/scripts" - }, - { - "name": "kong", - "mountPath": "/usr/local/kong" - } - ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "kong" + "hostname": "edgex-app-service-configurable-rules" } }, "strategy": {} - } - }, - { - "name": "edgex-app-service-configurable-rules", - "service": { - "ports": [ - { - "name": "tcp-48100", - "protocol": "TCP", - "port": 48100, - "targetPort": 48100 - } - ], - "selector": { - "app": "edgex-app-service-configurable-rules" - } - }, + } + }, + { + "name": "edgex-security-bootstrap-database", "deployment": { "selector": { "matchLabels": { - "app": "edgex-app-service-configurable-rules" + "app": "edgex-security-bootstrap-database" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-app-service-configurable-rules" + "app": "edgex-security-bootstrap-database" } }, "spec": { + "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-security-bootstrap-redis", + "type": "DirectoryOrCreate" + } + } + ], "containers": [ { - "name": "edgex-app-service-configurable-rules", - "image": "edgexfoundry/docker-app-service-configurable:1.3.1", - "ports": [ - { - "name": "tcp-48100", - "containerPort": 48100, - "protocol": "TCP" - } - ], + "name": "edgex-security-bootstrap-database", + "image": "edgexfoundry/docker-security-bootstrap-redis-go:1.3.1", "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "BINDING_PUBLISHTOPIC", - "value": "events" + "name": "SERVICE_HOST", + "value": "edgex-security-bootstrap-database" }, { - "name": "SERVICE_HOST", - "value": "edgex-app-service-configurable-rules" + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-security-bootstrap-redis/secrets-token.json" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" }, { - "name": "EDGEX_PROFILE", - "value": "rules-engine" + "name": "tmpfs-volume2", + "mountPath": "/vault" }, { - "name": "SERVICE_PORT", - "value": "48100" + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" }, { - "name": "MESSAGEBUS_SUBSCRIBEHOST_HOST", - "value": "edgex-core-data" + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-security-bootstrap-redis" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-app-service-configurable-rules" + "hostname": "edgex-security-bootstrap-database" } }, "strategy": {} } }, { - "name": "edgex-sys-mgmt-agent", + "name": "edgex-redis", "service": { "ports": [ { - "name": "tcp-48090", + "name": "tcp-6379", "protocol": "TCP", - "port": 48090, - "targetPort": 48090 + "port": 6379, + "targetPort": 6379 } ], "selector": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-redis" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-redis" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-sys-mgmt-agent" + "app": "edgex-redis" } }, "spec": { + "volumes": [ + { + "name": "db-data", + "emptyDir": {} + } + ], "containers": [ { - "name": "edgex-sys-mgmt-agent", - "image": "edgexfoundry/docker-sys-mgmt-agent-go:1.3.1", + "name": "edgex-redis", + "image": "redis:6.0.9-alpine", "ports": [ { - "name": "tcp-48090", - "containerPort": 48090, + "name": "tcp-6379", + "containerPort": 6379, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], - "env": [ - { - "name": "METRICSMECHANISM", - "value": "executor" - }, - { - "name": "SERVICE_HOST", - "value": "edgex-sys-mgmt-agent" - }, + "resources": {}, + "volumeMounts": [ { - "name": "EXECUTORPATH", - "value": "/sys-mgmt-executor" + "name": "db-data", + "mountPath": "/data" } ], - "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-sys-mgmt-agent" + "hostname": "edgex-redis" } }, "strategy": {} } }, { - "name": "edgex-vault", + "name": "edgex-support-notifications", "service": { "ports": [ { - "name": "tcp-8200", + "name": "tcp-48060", "protocol": "TCP", - "port": 8200, - "targetPort": 8200 + "port": 48060, + "targetPort": 48060 } ], "selector": { - "app": "edgex-vault" + "app": "edgex-support-notifications" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-vault" + "app": "edgex-support-notifications" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-vault" + "app": "edgex-support-notifications" } }, "spec": { "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/edgex-vault", + "path": "/tmp/edgex/secrets/ca", "type": "DirectoryOrCreate" } }, { - "name": "vault-file", - "emptyDir": {} - }, - { - "name": "vault-init", - "emptyDir": {} - }, - { - "name": "vault-logs", - "emptyDir": {} + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-support-notifications", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "edgex-vault", - "image": "vault:1.5.3", + "name": "edgex-support-notifications", + "image": "edgexfoundry/docker-support-notifications-go:1.3.1", "ports": [ { - "name": "tcp-8200", - "containerPort": 8200, + "name": "tcp-48060", + "containerPort": 48060, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "VAULT_UI", - "value": "true" - }, - { - "name": "VAULT_ADDR", - "value": "https://edgex-vault:8200" + "name": "SERVICE_HOST", + "value": "edgex-support-notifications" }, { - "name": "VAULT_CONFIG_DIR", - "value": "/vault/config" + "name": "SECRETSTORE_TOKENFILE", + "value": "/tmp/edgex/secrets/edgex-support-notifications/secrets-token.json" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/vault/config" - }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/edgex-vault" - }, - { - "name": "vault-file", - "mountPath": "/vault/file" - }, - { - "name": "vault-init", - "mountPath": "/vault/init" + "mountPath": "/tmp/edgex/secrets/ca" }, { - "name": "vault-logs", - "mountPath": "/vault/logs" + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-support-notifications" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-vault" + "hostname": "edgex-support-notifications" } }, "strategy": {} } }, { - "name": "edgex-vault-worker", + "name": "kong-db", + "service": { + "ports": [ + { + "name": "tcp-5432", + "protocol": "TCP", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "app": "kong-db" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-vault-worker" + "app": "kong-db" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-vault-worker" + "app": "kong-db" } }, "spec": { @@ -10742,203 +10608,212 @@ "emptyDir": {} }, { - "name": "consul-scripts", + "name": "tmpfs-volume3", "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets", - "type": "DirectoryOrCreate" - } - }, - { - "name": "vault-config", + "name": "postgres-data", "emptyDir": {} } ], "containers": [ { - "name": "edgex-vault-worker", - "image": "edgexfoundry/docker-security-secretstore-setup-go:1.3.1", + "name": "kong-db", + "image": "postgres:12.3-alpine", + "ports": [ + { + "name": "tcp-5432", + "containerPort": 5432, + "protocol": "TCP" + } + ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "SECRETSTORE_SETUP_DONE_FLAG", - "value": "/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done" + "name": "POSTGRES_USER", + "value": "kong" + }, + { + "name": "POSTGRES_DB", + "value": "kong" + }, + { + "name": "POSTGRES_PASSWORD", + "value": "kong" } ], "resources": {}, "volumeMounts": [ { "name": "tmpfs-volume1", - "mountPath": "/run" - }, - { - "name": "tmpfs-volume2", - "mountPath": "/vault" + "mountPath": "/var/run" }, - { - "name": "consul-scripts", - "mountPath": "/consul/scripts" + { + "name": "tmpfs-volume2", + "mountPath": "/tmp" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets" + "name": "tmpfs-volume3", + "mountPath": "/run" }, { - "name": "vault-config", - "mountPath": "/vault/config" + "name": "postgres-data", + "mountPath": "/var/lib/postgresql/data" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-vault-worker" + "hostname": "kong-db" } }, "strategy": {} } }, { - "name": "edgex-core-metadata", - "service": { - "ports": [ - { - "name": "tcp-48081", - "protocol": "TCP", - "port": 48081, - "targetPort": 48081 - } - ], - "selector": { - "app": "edgex-core-metadata" - } - }, + "name": "edgex-secrets-setup", "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-metadata" + "app": "edgex-secrets-setup" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-metadata" + "app": "edgex-secrets-setup" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "secrets-setup-cache", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/ca", + "path": "/tmp/edgex/secrets", "type": "DirectoryOrCreate" } }, { - "name": "anonymous-volume2", - "hostPath": { - "path": "/tmp/edgex/secrets/edgex-core-metadata", - "type": "DirectoryOrCreate" - } + "name": "vault-init", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-metadata", - "image": "edgexfoundry/docker-core-metadata-go:1.3.1", - "ports": [ - { - "name": "tcp-48081", - "containerPort": 48081, - "protocol": "TCP" - } - ], + "name": "edgex-secrets-setup", + "image": "edgexfoundry/docker-security-secrets-setup-go:1.3.1", "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], - "env": [ + "resources": {}, + "volumeMounts": [ { - "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/edgex-core-metadata/secrets-token.json" + "name": "tmpfs-volume1", + "mountPath": "/tmp" }, { - "name": "NOTIFICATIONS_SENDER", - "value": "edgex-core-metadata" + "name": "tmpfs-volume2", + "mountPath": "/run" }, { - "name": "SERVICE_HOST", - "value": "edgex-core-metadata" - } - ], - "resources": {}, - "volumeMounts": [ + "name": "secrets-setup-cache", + "mountPath": "/etc/edgex/pki" + }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/ca" + "mountPath": "/tmp/edgex/secrets" }, { - "name": "anonymous-volume2", - "mountPath": "/tmp/edgex/secrets/edgex-core-metadata" + "name": "vault-init", + "mountPath": "/vault/init" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-metadata" + "hostname": "edgex-secrets-setup" } }, "strategy": {} } }, { - "name": "edgex-core-consul", + "name": "kong", "service": { "ports": [ { - "name": "tcp-8500", + "name": "tcp-8000", "protocol": "TCP", - "port": 8500, - "targetPort": 8500 + "port": 8000, + "targetPort": 8000 + }, + { + "name": "tcp-8001", + "protocol": "TCP", + "port": 8001, + "targetPort": 8001 + }, + { + "name": "tcp-8443", + "protocol": "TCP", + "port": 8443, + "targetPort": 8443 + }, + { + "name": "tcp-8444", + "protocol": "TCP", + "port": 8444, + "targetPort": 8444 } ], "selector": { - "app": "edgex-core-consul" + "app": "kong" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-consul" + "app": "kong" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-consul" + "app": "kong" } }, "spec": { "volumes": [ { - "name": "consul-config", + "name": "tmpfs-volume1", "emptyDir": {} }, { - "name": "consul-data", + "name": "tmpfs-volume2", "emptyDir": {} }, { @@ -10946,101 +10821,100 @@ "emptyDir": {} }, { - "name": "anonymous-volume1", - "hostPath": { - "path": "/tmp/edgex/secrets/ca", - "type": "DirectoryOrCreate" - } - }, - { - "name": "anonymous-volume2", - "hostPath": { - "path": "/tmp/edgex/secrets/edgex-consul", - "type": "DirectoryOrCreate" - } - }, - { - "name": "anonymous-volume3", - "hostPath": { - "path": "/tmp/edgex/secrets/edgex-kong", - "type": "DirectoryOrCreate" - } - }, - { - "name": "anonymous-volume4", - "hostPath": { - "path": "/tmp/edgex/secrets/edgex-vault", - "type": "DirectoryOrCreate" - } + "name": "kong", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-consul", - "image": "edgexfoundry/docker-edgex-consul:1.3.0", + "name": "kong", + "image": "kong:2.0.5", "ports": [ { - "name": "tcp-8500", - "containerPort": 8500, + "name": "tcp-8000", + "containerPort": 8000, + "protocol": "TCP" + }, + { + "name": "tcp-8001", + "containerPort": 8001, + "protocol": "TCP" + }, + { + "name": "tcp-8443", + "containerPort": 8443, + "protocol": "TCP" + }, + { + "name": "tcp-8444", + "containerPort": 8444, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "EDGEX_SECURE", - "value": "true" + "name": "KONG_ADMIN_LISTEN", + "value": "0.0.0.0:8001, 0.0.0.0:8444 ssl" }, { - "name": "SECRETSTORE_SETUP_DONE_FLAG", - "value": "/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done" + "name": "KONG_DATABASE", + "value": "postgres" }, { - "name": "EDGEX_DB", - "value": "redis" - } - ], - "resources": {}, - "volumeMounts": [ + "name": "KONG_PG_HOST", + "value": "kong-db" + }, { - "name": "consul-config", - "mountPath": "/consul/config" + "name": "KONG_PG_PASSWORD", + "value": "kong" }, { - "name": "consul-data", - "mountPath": "/consul/data" + "name": "KONG_PROXY_ACCESS_LOG", + "value": "/dev/stdout" }, { - "name": "consul-scripts", - "mountPath": "/consul/scripts" + "name": "KONG_PROXY_ERROR_LOG", + "value": "/dev/stderr" }, { - "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/ca" + "name": "KONG_ADMIN_ACCESS_LOG", + "value": "/dev/stdout" }, { - "name": "anonymous-volume2", - "mountPath": "/tmp/edgex/secrets/edgex-consul" + "name": "KONG_ADMIN_ERROR_LOG", + "value": "/dev/stderr" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" }, { - "name": "anonymous-volume3", - "mountPath": "/tmp/edgex/secrets/edgex-kong" + "name": "tmpfs-volume2", + "mountPath": "/tmp" }, { - "name": "anonymous-volume4", - "mountPath": "/tmp/edgex/secrets/edgex-vault" + "name": "consul-scripts", + "mountPath": "/consul/scripts" + }, + { + "name": "kong", + "mountPath": "/usr/local/kong" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-consul" + "hostname": "kong" } }, "strategy": {} @@ -11105,19 +10979,11 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ - { - "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", - "value": "edgex-core-data" - }, - { - "name": "INTERVALACTIONS_SCRUBAGED_HOST", - "value": "edgex-core-data" - }, { "name": "SERVICE_HOST", "value": "edgex-support-scheduler" @@ -11125,6 +10991,14 @@ { "name": "SECRETSTORE_TOKENFILE", "value": "/tmp/edgex/secrets/edgex-support-scheduler/secrets-token.json" + }, + { + "name": "INTERVALACTIONS_SCRUBPUSHED_HOST", + "value": "edgex-core-data" + }, + { + "name": "INTERVALACTIONS_SCRUBAGED_HOST", + "value": "edgex-core-data" } ], "resources": {}, @@ -11148,98 +11022,118 @@ } }, { - "name": "edgex-device-rest", + "name": "edgex-sys-mgmt-agent", "service": { "ports": [ { - "name": "tcp-49986", + "name": "tcp-48090", "protocol": "TCP", - "port": 49986, - "targetPort": 49986 + "port": 48090, + "targetPort": 48090 } ], "selector": { - "app": "edgex-device-rest" + "app": "edgex-sys-mgmt-agent" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-device-rest" + "app": "edgex-sys-mgmt-agent" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-device-rest" + "app": "edgex-sys-mgmt-agent" } }, "spec": { "containers": [ { - "name": "edgex-device-rest", - "image": "edgexfoundry/docker-device-rest-go:1.2.1", + "name": "edgex-sys-mgmt-agent", + "image": "edgexfoundry/docker-sys-mgmt-agent-go:1.3.1", "ports": [ { - "name": "tcp-49986", - "containerPort": 49986, + "name": "tcp-48090", + "containerPort": 48090, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-device-rest" + "value": "edgex-sys-mgmt-agent" + }, + { + "name": "METRICSMECHANISM", + "value": "executor" + }, + { + "name": "EXECUTORPATH", + "value": "/sys-mgmt-executor" } ], "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-device-rest" + "hostname": "edgex-sys-mgmt-agent" } }, "strategy": {} } }, { - "name": "edgex-core-command", + "name": "edgex-core-consul", "service": { "ports": [ { - "name": "tcp-48082", + "name": "tcp-8500", "protocol": "TCP", - "port": 48082, - "targetPort": 48082 + "port": 8500, + "targetPort": 8500 } ], "selector": { - "app": "edgex-core-command" + "app": "edgex-core-consul" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-command" + "app": "edgex-core-consul" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-command" + "app": "edgex-core-consul" } }, "spec": { "volumes": [ + { + "name": "consul-config", + "emptyDir": {} + }, + { + "name": "consul-data", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { @@ -11250,227 +11144,252 @@ { "name": "anonymous-volume2", "hostPath": { - "path": "/tmp/edgex/secrets/edgex-core-command", + "path": "/tmp/edgex/secrets/edgex-consul", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume3", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-kong", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume4", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-vault", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-core-command", - "image": "edgexfoundry/docker-core-command-go:1.3.1", + "name": "edgex-core-consul", + "image": "edgexfoundry/docker-edgex-consul:1.3.0", "ports": [ { - "name": "tcp-48082", - "containerPort": 48082, + "name": "tcp-8500", + "containerPort": 8500, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/edgex-core-command/secrets-token.json" + "name": "SECRETSTORE_SETUP_DONE_FLAG", + "value": "/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done" }, { - "name": "SERVICE_HOST", - "value": "edgex-core-command" + "name": "EDGEX_DB", + "value": "redis" + }, + { + "name": "EDGEX_SECURE", + "value": "true" } ], "resources": {}, "volumeMounts": [ + { + "name": "consul-config", + "mountPath": "/consul/config" + }, + { + "name": "consul-data", + "mountPath": "/consul/data" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" + }, { "name": "anonymous-volume1", "mountPath": "/tmp/edgex/secrets/ca" }, { "name": "anonymous-volume2", - "mountPath": "/tmp/edgex/secrets/edgex-core-command" + "mountPath": "/tmp/edgex/secrets/edgex-consul" + }, + { + "name": "anonymous-volume3", + "mountPath": "/tmp/edgex/secrets/edgex-kong" + }, + { + "name": "anonymous-volume4", + "mountPath": "/tmp/edgex/secrets/edgex-vault" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-command" + "hostname": "edgex-core-consul" } }, "strategy": {} } }, { - "name": "edgex-redis", - "service": { - "ports": [ - { - "name": "tcp-6379", - "protocol": "TCP", - "port": 6379, - "targetPort": 6379 - } - ], - "selector": { - "app": "edgex-redis" - } - }, + "name": "", "deployment": { "selector": { "matchLabels": { - "app": "edgex-redis" + "app": "" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-redis" + "app": "" } }, "spec": { "volumes": [ { - "name": "db-data", + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "consul-scripts", "emptyDir": {} } ], "containers": [ { - "name": "edgex-redis", - "image": "redis:6.0.9-alpine", - "ports": [ - { - "name": "tcp-6379", - "containerPort": 6379, - "protocol": "TCP" - } - ], + "name": "", + "image": "kong:2.0.5", "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], + "env": [ + { + "name": "KONG_DATABASE", + "value": "postgres" + }, + { + "name": "KONG_PG_HOST", + "value": "kong-db" + }, + { + "name": "KONG_PG_PASSWORD", + "value": "kong" + } + ], "resources": {}, "volumeMounts": [ { - "name": "db-data", - "mountPath": "/data" + "name": "tmpfs-volume1", + "mountPath": "/tmp" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" } ], "imagePullPolicy": "IfNotPresent" } - ], - "hostname": "edgex-redis" + ] } }, "strategy": {} } }, { - "name": "edgex-core-data", - "service": { - "ports": [ - { - "name": "tcp-5563", - "protocol": "TCP", - "port": 5563, - "targetPort": 5563 - }, - { - "name": "tcp-48080", - "protocol": "TCP", - "port": 48080, - "targetPort": 48080 - } - ], - "selector": { - "app": "edgex-core-data" - } - }, + "name": "edgex-vault-worker", "deployment": { "selector": { "matchLabels": { - "app": "edgex-core-data" + "app": "edgex-vault-worker" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-core-data" + "app": "edgex-vault-worker" } }, "spec": { "volumes": [ + { + "name": "tmpfs-volume1", + "emptyDir": {} + }, + { + "name": "tmpfs-volume2", + "emptyDir": {} + }, + { + "name": "consul-scripts", + "emptyDir": {} + }, { "name": "anonymous-volume1", "hostPath": { - "path": "/tmp/edgex/secrets/ca", + "path": "/tmp/edgex/secrets", "type": "DirectoryOrCreate" } }, { - "name": "anonymous-volume2", - "hostPath": { - "path": "/tmp/edgex/secrets/edgex-core-data", - "type": "DirectoryOrCreate" - } + "name": "vault-config", + "emptyDir": {} } ], "containers": [ { - "name": "edgex-core-data", - "image": "edgexfoundry/docker-core-data-go:1.3.1", - "ports": [ - { - "name": "tcp-5563", - "containerPort": 5563, - "protocol": "TCP" - }, - { - "name": "tcp-48080", - "containerPort": 48080, - "protocol": "TCP" - } - ], + "name": "edgex-vault-worker", + "image": "edgexfoundry/docker-security-secretstore-setup-go:1.3.1", "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "SERVICE_HOST", - "value": "edgex-core-data" - }, - { - "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/edgex-core-data/secrets-token.json" + "name": "SECRETSTORE_SETUP_DONE_FLAG", + "value": "/tmp/edgex/secrets/edgex-consul/.secretstore-setup-done" } ], "resources": {}, "volumeMounts": [ + { + "name": "tmpfs-volume1", + "mountPath": "/run" + }, + { + "name": "tmpfs-volume2", + "mountPath": "/vault" + }, + { + "name": "consul-scripts", + "mountPath": "/consul/scripts" + }, { "name": "anonymous-volume1", - "mountPath": "/tmp/edgex/secrets/ca" + "mountPath": "/tmp/edgex/secrets" }, { - "name": "anonymous-volume2", - "mountPath": "/tmp/edgex/secrets/edgex-core-data" + "name": "vault-config", + "mountPath": "/vault/config" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-core-data" + "hostname": "edgex-vault-worker" } }, "strategy": {} @@ -11519,7 +11438,7 @@ "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], @@ -11540,138 +11459,129 @@ } }, { - "name": "kong-db", + "name": "edgex-kuiper", "service": { "ports": [ { - "name": "tcp-5432", + "name": "tcp-20498", "protocol": "TCP", - "port": 5432, - "targetPort": 5432 + "port": 20498, + "targetPort": 20498 + }, + { + "name": "tcp-48075", + "protocol": "TCP", + "port": 48075, + "targetPort": 48075 } ], "selector": { - "app": "kong-db" + "app": "edgex-kuiper" } }, "deployment": { "selector": { "matchLabels": { - "app": "kong-db" + "app": "edgex-kuiper" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "kong-db" + "app": "edgex-kuiper" } }, "spec": { - "volumes": [ - { - "name": "tmpfs-volume1", - "emptyDir": {} - }, - { - "name": "tmpfs-volume2", - "emptyDir": {} - }, - { - "name": "tmpfs-volume3", - "emptyDir": {} - }, - { - "name": "postgres-data", - "emptyDir": {} - } - ], "containers": [ { - "name": "kong-db", - "image": "postgres:12.3-alpine", + "name": "edgex-kuiper", + "image": "emqx/kuiper:1.1.1-alpine", "ports": [ { - "name": "tcp-5432", - "containerPort": 5432, + "name": "tcp-20498", + "containerPort": 20498, + "protocol": "TCP" + }, + { + "name": "tcp-48075", + "containerPort": 48075, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "POSTGRES_USER", - "value": "kong" + "name": "EDGEX__DEFAULT__SERVICESERVER", + "value": "http://edgex-core-data:48080" }, { - "name": "POSTGRES_DB", - "value": "kong" + "name": "EDGEX__DEFAULT__TOPIC", + "value": "events" }, { - "name": "POSTGRES_PASSWORD", - "value": "kong" - } - ], - "resources": {}, - "volumeMounts": [ + "name": "KUIPER__BASIC__CONSOLELOG", + "value": "true" + }, { - "name": "tmpfs-volume1", - "mountPath": "/var/run" + "name": "KUIPER__BASIC__RESTPORT", + "value": "48075" }, { - "name": "tmpfs-volume2", - "mountPath": "/tmp" + "name": "EDGEX__DEFAULT__PORT", + "value": "5566" }, { - "name": "tmpfs-volume3", - "mountPath": "/run" + "name": "EDGEX__DEFAULT__PROTOCOL", + "value": "tcp" }, { - "name": "postgres-data", - "mountPath": "/var/lib/postgresql/data" + "name": "EDGEX__DEFAULT__SERVER", + "value": "edgex-app-service-configurable-rules" } ], + "resources": {}, "imagePullPolicy": "IfNotPresent" } ], - "hostname": "kong-db" + "hostname": "edgex-kuiper" } }, "strategy": {} } }, { - "name": "edgex-support-notifications", + "name": "edgex-core-metadata", "service": { "ports": [ { - "name": "tcp-48060", + "name": "tcp-48081", "protocol": "TCP", - "port": 48060, - "targetPort": 48060 + "port": 48081, + "targetPort": 48081 } ], "selector": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "deployment": { "selector": { "matchLabels": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "edgex-support-notifications" + "app": "edgex-core-metadata" } }, "spec": { @@ -11686,37 +11596,41 @@ { "name": "anonymous-volume2", "hostPath": { - "path": "/tmp/edgex/secrets/edgex-support-notifications", + "path": "/tmp/edgex/secrets/edgex-core-metadata", "type": "DirectoryOrCreate" } } ], "containers": [ { - "name": "edgex-support-notifications", - "image": "edgexfoundry/docker-support-notifications-go:1.3.1", + "name": "edgex-core-metadata", + "image": "edgexfoundry/docker-core-metadata-go:1.3.1", "ports": [ { - "name": "tcp-48060", - "containerPort": 48060, + "name": "tcp-48081", + "containerPort": 48081, "protocol": "TCP" } ], "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { "name": "SERVICE_HOST", - "value": "edgex-support-notifications" + "value": "edgex-core-metadata" + }, + { + "name": "NOTIFICATIONS_SENDER", + "value": "edgex-core-metadata" }, { "name": "SECRETSTORE_TOKENFILE", - "value": "/tmp/edgex/secrets/edgex-support-notifications/secrets-token.json" + "value": "/tmp/edgex/secrets/edgex-core-metadata/secrets-token.json" } ], "resources": {}, @@ -11727,83 +11641,169 @@ }, { "name": "anonymous-volume2", - "mountPath": "/tmp/edgex/secrets/edgex-support-notifications" + "mountPath": "/tmp/edgex/secrets/edgex-core-metadata" } ], "imagePullPolicy": "IfNotPresent" } ], - "hostname": "edgex-support-notifications" + "hostname": "edgex-core-metadata" } }, "strategy": {} } }, { - "name": "", + "name": "edgex-device-rest", + "service": { + "ports": [ + { + "name": "tcp-49986", + "protocol": "TCP", + "port": 49986, + "targetPort": 49986 + } + ], + "selector": { + "app": "edgex-device-rest" + } + }, "deployment": { "selector": { "matchLabels": { - "app": "" + "app": "edgex-device-rest" } }, "template": { "metadata": { "creationTimestamp": null, "labels": { - "app": "" + "app": "edgex-device-rest" + } + }, + "spec": { + "containers": [ + { + "name": "edgex-device-rest", + "image": "edgexfoundry/docker-device-rest-go:1.2.1", + "ports": [ + { + "name": "tcp-49986", + "containerPort": 49986, + "protocol": "TCP" + } + ], + "envFrom": [ + { + "configMapRef": { + "name": "common-variables" + } + } + ], + "env": [ + { + "name": "SERVICE_HOST", + "value": "edgex-device-rest" + } + ], + "resources": {}, + "imagePullPolicy": "IfNotPresent" + } + ], + "hostname": "edgex-device-rest" + } + }, + "strategy": {} + } + }, + { + "name": "edgex-proxy", + "deployment": { + "selector": { + "matchLabels": { + "app": "edgex-proxy" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "edgex-proxy" } }, "spec": { "volumes": [ { - "name": "tmpfs-volume1", + "name": "consul-scripts", "emptyDir": {} }, { - "name": "consul-scripts", - "emptyDir": {} + "name": "anonymous-volume1", + "hostPath": { + "path": "/tmp/edgex/secrets/ca", + "type": "DirectoryOrCreate" + } + }, + { + "name": "anonymous-volume2", + "hostPath": { + "path": "/tmp/edgex/secrets/edgex-security-proxy-setup", + "type": "DirectoryOrCreate" + } } ], "containers": [ { - "name": "", - "image": "kong:2.0.5", + "name": "edgex-proxy", + "image": "edgexfoundry/docker-security-proxy-setup-go:1.3.1", "envFrom": [ { "configMapRef": { - "name": "common-variable-hanoi" + "name": "common-variables" } } ], "env": [ { - "name": "KONG_DATABASE", - "value": "postgres" + "name": "SECRETSERVICE_SNIS", + "value": "edgex-kong" }, { - "name": "KONG_PG_HOST", - "value": "kong-db" + "name": "SECRETSERVICE_SERVER", + "value": "edgex-vault" }, { - "name": "KONG_PG_PASSWORD", + "name": "SECRETSERVICE_CACERTPATH", + "value": "/tmp/edgex/secrets/ca/ca.pem" + }, + { + "name": "SECRETSERVICE_TOKENPATH", + "value": "/tmp/edgex/secrets/edgex-security-proxy-setup/secrets-token.json" + }, + { + "name": "KONGURL_SERVER", "value": "kong" } ], "resources": {}, "volumeMounts": [ - { - "name": "tmpfs-volume1", - "mountPath": "/tmp" - }, { "name": "consul-scripts", "mountPath": "/consul/scripts" + }, + { + "name": "anonymous-volume1", + "mountPath": "/tmp/edgex/secrets/ca" + }, + { + "name": "anonymous-volume2", + "mountPath": "/tmp/edgex/secrets/edgex-security-proxy-setup" } ], "imagePullPolicy": "IfNotPresent" } - ] + ], + "hostname": "edgex-proxy" } }, "strategy": {} diff --git a/pkg/controller/platformadmin/platformadmin_controller.go b/pkg/controller/platformadmin/platformadmin_controller.go index 550f85e48ef..542c147c0f2 100644 --- a/pkg/controller/platformadmin/platformadmin_controller.go +++ b/pkg/controller/platformadmin/platformadmin_controller.go @@ -28,10 +28,12 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + kjson "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" + "k8s.io/kubectl/pkg/scheme" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -55,6 +57,11 @@ func init() { flag.IntVar(&concurrentReconciles, "platformadmin-workers", concurrentReconciles, "Max concurrent workers for PlatformAdmin controller.") } +func Format(format string, args ...interface{}) string { + s := fmt.Sprintf(format, args...) + return fmt.Sprintf("%s: %s", ControllerName, s) +} + var ( concurrentReconciles = 3 controllerKind = iotv1alpha2.SchemeGroupVersion.WithKind("PlatformAdmin") @@ -66,24 +73,47 @@ const ( LabelConfigmap = "Configmap" LabelService = "Service" LabelDeployment = "Deployment" + LabelFramework = "Framework" AnnotationServiceTopologyKey = "openyurt.io/topologyKeys" AnnotationServiceTopologyValueNodePool = "openyurt.io/nodepool" - ConfigMapName = "common-variables" + ConfigMapName = "common-variables" + FrameworkName = "platformadmin-framework" + FrameworkFinalizer = "kubernetes.io/platformadmin-framework" ) -func Format(format string, args ...interface{}) string { - s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) +// PlatformAdminFramework is the framework of platformadmin, +// it contains all configs of configmaps, services and yurtappsets. +// PlatformAdmin will customize the configuration based on this structure. +type PlatformAdminFramework struct { + runtime.TypeMeta `json:",inline"` + + name string + security bool + Components []*config.Component `yaml:"components,omitempty" json:"components,omitempty"` + ConfigMaps []corev1.ConfigMap `yaml:"configMaps,omitempty" json:"configMaps,omitempty"` +} + +// A function written to implement the yaml serializer interface, which is not actually useful +func (p *PlatformAdminFramework) DeepCopyObject() runtime.Object { + copy := p.DeepCopy() + return © } -// ReconcilePlatformAdmin reconciles a PlatformAdmin object +// A function written to implement the yaml serializer interface, which is not actually useful +func (p *PlatformAdminFramework) DeepCopy() PlatformAdminFramework { + newObj := *p + return newObj +} + +// ReconcilePlatformAdmin reconciles a PlatformAdmin object. type ReconcilePlatformAdmin struct { client.Client - scheme *runtime.Scheme - recorder record.EventRecorder - Configration config.PlatformAdminControllerConfiguration + scheme *runtime.Scheme + recorder record.EventRecorder + yamlSerializer *kjson.Serializer + Configration config.PlatformAdminControllerConfiguration } var _ reconcile.Reconciler = &ReconcilePlatformAdmin{} @@ -102,10 +132,11 @@ func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { return &ReconcilePlatformAdmin{ - Client: utilclient.NewClientFromManager(mgr, ControllerName), - scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(ControllerName), - Configration: c.ComponentConfig.PlatformAdminController, + Client: utilclient.NewClientFromManager(mgr, ControllerName), + scheme: mgr.GetScheme(), + recorder: mgr.GetEventRecorderFor(ControllerName), + yamlSerializer: kjson.NewSerializerWithOptions(kjson.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, kjson.SerializerOptions{Yaml: true, Pretty: true}), + Configration: c.ComponentConfig.PlatformAdminController, } } @@ -149,9 +180,9 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } - klog.V(4).Info("registering the field indexers of platformadmin controller") + klog.V(4).Infof(Format("registering the field indexers of platformadmin controller")) if err := util.RegisterFieldIndexers(mgr.GetFieldIndexer()); err != nil { - klog.Errorf("failed to register field indexers for platformadmin controller, %v", err) + klog.Errorf(Format("failed to register field indexers for platformadmin controller, %v", err)) return nil } @@ -212,12 +243,12 @@ func (r *ReconcilePlatformAdmin) Reconcile(ctx context.Context, request reconcil func (r *ReconcilePlatformAdmin) reconcileDelete(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin) (reconcile.Result, error) { klog.V(4).Infof(Format("ReconcileDelete PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) yas := &appsv1alpha1.YurtAppSet{} - var desiredComponents []*config.Component - if platformAdmin.Spec.Security { - desiredComponents = r.Configration.SecurityComponents[platformAdmin.Spec.Version] - } else { - desiredComponents = r.Configration.NoSectyComponents[platformAdmin.Spec.Version] + + platformAdminFramework, err := r.syncFramework(ctx, platformAdmin) + if err != nil { + return reconcile.Result{}, errors.Wrapf(err, "unexpected error while synchronizing customize framework for %s", platformAdmin.Namespace+"/"+platformAdmin.Name) } + desiredComponents := platformAdminFramework.Components additionalComponents, err := annotationToComponent(platformAdmin.Annotations) if err != nil { @@ -264,9 +295,19 @@ func (r *ReconcilePlatformAdmin) reconcileNormal(ctx context.Context, platformAd klog.V(4).Infof(Format("ReconcileNormal PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) controllerutil.AddFinalizer(platformAdmin, iotv1alpha2.PlatformAdminFinalizer) - platformAdmin.Status.Initialized = true + platformAdminStatus.Initialized = true + + // Note that this configmap is different from the one below, which is used to customize the edgex framework + // Sync configmap of edgex confiruation during initialization + // This framework pointer is needed to synchronize user-modified edgex configurations + platformAdminFramework, err := r.syncFramework(ctx, platformAdmin) + if err != nil { + return reconcile.Result{}, errors.Wrapf(err, "unexpected error while synchronizing customize framework for %s", platformAdmin.Namespace+"/"+platformAdmin.Name) + } + + // Reconcile configmap of edgex confiruation klog.V(4).Infof(Format("ReconcileConfigmap PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) - if ok, err := r.reconcileConfigmap(ctx, platformAdmin, platformAdminStatus); !ok { + if ok, err := r.reconcileConfigmap(ctx, platformAdmin, platformAdminStatus, platformAdminFramework); !ok { if err != nil { util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ConfigmapAvailableCondition, corev1.ConditionFalse, iotv1alpha2.ConfigmapProvisioningFailedReason, err.Error())) return reconcile.Result{}, errors.Wrapf(err, @@ -277,8 +318,9 @@ func (r *ReconcilePlatformAdmin) reconcileNormal(ctx context.Context, platformAd } util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ConfigmapAvailableCondition, corev1.ConditionTrue, "", "")) + // Reconcile component of edgex confiruation klog.V(4).Infof(Format("ReconcileComponent PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) - if ok, err := r.reconcileComponent(ctx, platformAdmin, platformAdminStatus); !ok { + if ok, err := r.reconcileComponent(ctx, platformAdmin, platformAdminStatus, platformAdminFramework); !ok { if err != nil { util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ComponentAvailableCondition, corev1.ConditionFalse, iotv1alpha2.ComponentProvisioningReason, err.Error())) return reconcile.Result{}, errors.Wrapf(err, @@ -289,6 +331,7 @@ func (r *ReconcilePlatformAdmin) reconcileNormal(ctx context.Context, platformAd } util.SetPlatformAdminCondition(platformAdminStatus, util.NewPlatformAdminCondition(iotv1alpha2.ComponentAvailableCondition, corev1.ConditionTrue, "", "")) + // Update the metadata of PlatformAdmin platformAdminStatus.Ready = true if err := r.Client.Update(ctx, platformAdmin); err != nil { klog.Errorf(Format("Update PlatformAdmin %s error %v", klog.KObj(platformAdmin), err)) @@ -298,22 +341,17 @@ func (r *ReconcilePlatformAdmin) reconcileNormal(ctx context.Context, platformAd return reconcile.Result{}, nil } -func (r *ReconcilePlatformAdmin) reconcileConfigmap(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, _ *iotv1alpha2.PlatformAdminStatus) (bool, error) { +func (r *ReconcilePlatformAdmin) reconcileConfigmap(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, _ *iotv1alpha2.PlatformAdminStatus, platformAdminFramework *PlatformAdminFramework) (bool, error) { var configmaps []corev1.ConfigMap needConfigMaps := make(map[string]struct{}) + configmaps = platformAdminFramework.ConfigMaps - if platformAdmin.Spec.Security { - configmaps = r.Configration.SecurityConfigMaps[platformAdmin.Spec.Version] - } else { - configmaps = r.Configration.NoSectyConfigMaps[platformAdmin.Spec.Version] - } - for _, configmap := range configmaps { - // Supplement runtime information + for i, configmap := range configmaps { configmap.Namespace = platformAdmin.Namespace configmap.Labels = make(map[string]string) configmap.Labels[iotv1alpha2.LabelPlatformAdminGenerate] = LabelConfigmap - _, err := controllerutil.CreateOrUpdate(ctx, r.Client, &configmap, func() error { + configmap.Data = platformAdminFramework.ConfigMaps[i].Data return controllerutil.SetOwnerReference(platformAdmin, &configmap, (r.Scheme())) }) if err != nil { @@ -335,16 +373,12 @@ func (r *ReconcilePlatformAdmin) reconcileConfigmap(ctx context.Context, platfor return true, nil } -func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminStatus *iotv1alpha2.PlatformAdminStatus) (bool, error) { +func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminStatus *iotv1alpha2.PlatformAdminStatus, platformAdminFramework *PlatformAdminFramework) (bool, error) { var desireComponents []*config.Component needComponents := make(map[string]struct{}) var readyComponent int32 = 0 - if platformAdmin.Spec.Security { - desireComponents = r.Configration.SecurityComponents[platformAdmin.Spec.Version] - } else { - desireComponents = r.Configration.NoSectyComponents[platformAdmin.Spec.Version] - } + desireComponents = platformAdminFramework.Components additionalComponents, err := annotationToComponent(platformAdmin.Annotations) if err != nil { @@ -359,7 +393,6 @@ func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platfor platformAdminStatus.UnreadyComponentNum = int32(len(desireComponents)) - readyComponent }() -NextC: for _, desireComponent := range desireComponents { readyService := false readyDeployment := false @@ -370,7 +403,13 @@ NextC: } readyService = true - yas := &appsv1alpha1.YurtAppSet{} + yas := &appsv1alpha1.YurtAppSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: desireComponent.Name, + Namespace: platformAdmin.Namespace, + }, + } + err := r.Get( ctx, types.NamespacedName{ @@ -388,6 +427,9 @@ NextC: } else { oldYas := yas.DeepCopy() + // Refresh the YurtAppSet according to the user-defined configuration + yas.Spec.WorkloadTemplate.DeploymentTemplate.Spec = *desireComponent.Deployment + if _, ok := yas.Status.PoolReplicas[platformAdmin.Spec.PoolName]; ok { if yas.Status.ReadyReplicas == yas.Status.Replicas { readyDeployment = true @@ -395,7 +437,6 @@ NextC: readyComponent++ } } - continue NextC } pool := appsv1alpha1.Pool{ Name: platformAdmin.Spec.PoolName, @@ -465,7 +506,6 @@ func (r *ReconcilePlatformAdmin) handleService(ctx context.Context, platformAdmi Name: component.Name, Namespace: platformAdmin.Namespace, }, - Spec: *component.Service, } service.Labels[iotv1alpha2.LabelPlatformAdminGenerate] = LabelService service.Annotations[AnnotationServiceTopologyKey] = AnnotationServiceTopologyValueNodePool @@ -475,10 +515,10 @@ func (r *ReconcilePlatformAdmin) handleService(ctx context.Context, platformAdmi r.Client, service, func() error { + service.Spec = *component.Service return controllerutil.SetOwnerReference(platformAdmin, service, r.Scheme()) }, ) - if err != nil { return nil, err } @@ -486,6 +526,13 @@ func (r *ReconcilePlatformAdmin) handleService(ctx context.Context, platformAdmi } func (r *ReconcilePlatformAdmin) handleYurtAppSet(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, component *config.Component) (*appsv1alpha1.YurtAppSet, error) { + // It is possible that the component does not need deployment. + // Therefore, you need to be careful when calling this function. + // It is still possible for deployment to be nil when there is no error! + if component.Deployment == nil { + return nil, nil + } + yas := &appsv1alpha1.YurtAppSet{ ObjectMeta: metav1.ObjectMeta{ Labels: make(map[string]string), @@ -600,3 +647,116 @@ func annotationToComponent(annotation map[string]string) ([]*config.Component, e return components, nil } + +func (r *ReconcilePlatformAdmin) syncFramework(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin) (*PlatformAdminFramework, error) { + klog.V(6).Infof(Format("Synchronize the customize framework information for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + + // Try to get the configmap that represents the framework + platformAdminFramework := &PlatformAdminFramework{ + // The configmap that represents framework is named with the framework prefix and the version name + name: FrameworkName, + } + + // Check if the configmap that represents framework is found + cm := &corev1.ConfigMap{} + if err := r.Get(ctx, types.NamespacedName{Namespace: platformAdmin.Namespace, Name: platformAdminFramework.name}, cm); err != nil { + if apierrors.IsNotFound(err) { + // If the configmap that represents framework is not found, + // need to create it by standard configuration + err = r.initFramework(ctx, platformAdmin, platformAdminFramework) + if err != nil { + klog.Errorf(Format("Init framework for PlatformAdmin %s/%s error %v", platformAdmin.Namespace, platformAdmin.Name, err)) + return nil, err + } + return platformAdminFramework, nil + } + klog.Errorf(Format("Get framework for PlatformAdmin %s/%s error %v", platformAdmin.Namespace, platformAdmin.Name, err)) + return nil, err + } + + // For better serialization, the serialization method of the Kubernetes runtime library is used + err := runtime.DecodeInto(r.yamlSerializer, []byte(cm.Data["framework"]), platformAdminFramework) + if err != nil { + klog.Errorf(Format("Decode framework for PlatformAdmin %s/%s error %v", platformAdmin.Namespace, platformAdmin.Name, err)) + return nil, err + } + + // If PlatformAdmin is about to be deleted, remove Finalizer from the framework. + // If not deleted, the owner reference is synchronized. + if platformAdmin.DeletionTimestamp != nil { + _, err = controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error { + // During the deletion phase, ensure that data in the framework is read before deletion + // The following code removes the finalizer, allowing the framework to be deleted (since we read out its data above). + controllerutil.RemoveFinalizer(cm, FrameworkFinalizer) + return nil + }) + if err != nil { + klog.Errorf(Format("Failed to remove finalizer of framework configmap for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + return nil, err + } + } else { + hasOwnerReference := false + for _, ref := range cm.ObjectMeta.OwnerReferences { + if ref.Kind == platformAdmin.Kind && ref.Name == platformAdmin.Name { + hasOwnerReference = true + } + } + if !hasOwnerReference { + _, err = controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error { + return controllerutil.SetOwnerReference(platformAdmin, cm, r.scheme) + }) + if err != nil { + klog.Errorf(Format("Failed to add owner reference of framework configmap for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + return nil, err + } + } + } + + return platformAdminFramework, nil +} + +// initFramework initializes the framework information for PlatformAdmin +func (r *ReconcilePlatformAdmin) initFramework(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminFramework *PlatformAdminFramework) error { + klog.V(6).Infof(Format("Initializes the standard framework information for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + + // Use standard configurations to build the framework + platformAdminFramework.security = platformAdmin.Spec.Security + if platformAdminFramework.security { + platformAdminFramework.ConfigMaps = r.Configration.SecurityConfigMaps[platformAdmin.Spec.Version] + platformAdminFramework.Components = r.Configration.SecurityComponents[platformAdmin.Spec.Version] + } else { + platformAdminFramework.ConfigMaps = r.Configration.NoSectyConfigMaps[platformAdmin.Spec.Version] + platformAdminFramework.Components = r.Configration.NoSectyComponents[platformAdmin.Spec.Version] + } + + // For better serialization, the serialization method of the Kubernetes runtime library is used + data, err := runtime.Encode(r.yamlSerializer, platformAdminFramework) + if err != nil { + klog.Errorf(Format("Failed to marshal framework for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + return err + } + + // Create the configmap that represents framework + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: platformAdminFramework.name, + Namespace: platformAdmin.Namespace, + }, + } + cm.Labels = make(map[string]string) + cm.Labels[iotv1alpha2.LabelPlatformAdminGenerate] = LabelFramework + cm.Data = make(map[string]string) + cm.Data["framework"] = string(data) + // Creates configmap on behalf of the framework, which is called only once upon creation + _, err = controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error { + // We need to control the deletion time of the framework, + // because we must ensure that its data is read before deleting it. + controllerutil.AddFinalizer(cm, FrameworkFinalizer) + return controllerutil.SetOwnerReference(platformAdmin, cm, r.Scheme()) + }) + if err != nil { + klog.Errorf(Format("Failed to create or update framework configmap for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + return err + } + return nil +} From 4fe22e499bc959d23514b1db20d3de0cdc840277 Mon Sep 17 00:00:00 2001 From: dsy3502 Date: Mon, 17 Jul 2023 10:16:17 +0800 Subject: [PATCH 56/93] str replaced by const (#1613) --- pkg/webhook/yurtappset/v1alpha1/yurtappset_validation.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/webhook/yurtappset/v1alpha1/yurtappset_validation.go b/pkg/webhook/yurtappset/v1alpha1/yurtappset_validation.go index 50a3e57c1a7..5179f244c94 100644 --- a/pkg/webhook/yurtappset/v1alpha1/yurtappset_validation.go +++ b/pkg/webhook/yurtappset/v1alpha1/yurtappset_validation.go @@ -26,6 +26,8 @@ import ( "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) +const YurtAppSetKind = "YurtAppSet" + // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. func (webhook *YurtAppSetHandler) ValidateCreate(ctx context.Context, obj runtime.Object) error { appset, ok := obj.(*v1alpha1.YurtAppSet) @@ -34,7 +36,7 @@ func (webhook *YurtAppSetHandler) ValidateCreate(ctx context.Context, obj runtim } if allErrs := validateYurtAppSet(webhook.Client, appset); len(allErrs) > 0 { - return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind("YurtAppSet").GroupKind(), appset.Name, allErrs) + return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind(YurtAppSetKind).GroupKind(), appset.Name, allErrs) } return nil @@ -55,7 +57,7 @@ func (webhook *YurtAppSetHandler) ValidateUpdate(ctx context.Context, oldObj, ne updateErrorList := ValidateYurtAppSetUpdate(newAppSet, oldAppSet) if allErrs := append(validationErrorList, updateErrorList...); len(allErrs) > 0 { - return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind("YurtAppSet").GroupKind(), newAppSet.Name, allErrs) + return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind(YurtAppSetKind).GroupKind(), newAppSet.Name, allErrs) } return nil From 107a56638a09f39eb441df63ac5905a9d6097c3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 11:32:18 +0800 Subject: [PATCH 57/93] build(deps): bump ossf/scorecard-action from 2.1.3 to 2.2.0 (#1571) Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.1.3 to 2.2.0. - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/80e868c13c90f172d68d1f4501dee99e2479f7af...08b4669551908b1024bb425080c797723083c031) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sonarcloud.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yaml b/.github/workflows/sonarcloud.yaml index b46c11544b4..ff4aebc436f 100644 --- a/.github/workflows/sonarcloud.yaml +++ b/.github/workflows/sonarcloud.yaml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3 + uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 with: results_file: results.sarif results_format: sarif From 1845a2e5d2442a12eb973f32bb7a291a1902371f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 11:33:18 +0800 Subject: [PATCH 58/93] build(deps): bump golang.org/x/sys from 0.8.0 to 0.10.0 (#1588) Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.8.0 to 0.10.0. - [Commits](https://github.com/golang/sys/compare/v0.8.0...v0.10.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c0fe5350d2c..45c971a5821 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 - golang.org/x/sys v0.8.0 + golang.org/x/sys v0.10.0 google.golang.org/grpc v1.56.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/square/go-jose.v2 v2.6.0 diff --git a/go.sum b/go.sum index ae74c11aceb..0b3b098d5af 100644 --- a/go.sum +++ b/go.sum @@ -921,8 +921,8 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= From fe46a9d1a52686aaf5de205bcbcc39d65d14e937 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:13:18 +0800 Subject: [PATCH 59/93] build(deps): bump google.golang.org/grpc from 1.56.0 to 1.56.2 (#1593) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.56.0 to 1.56.2. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.56.0...v1.56.2) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 45c971a5821..f79da3cf042 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 golang.org/x/sys v0.10.0 - google.golang.org/grpc v1.56.0 + google.golang.org/grpc v1.56.2 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 0b3b098d5af..4c71d04330b 100644 --- a/go.sum +++ b/go.sum @@ -1063,8 +1063,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE= -google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= +google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 4efa1bf02c689f2746a88547dfdff32a04b3a561 Mon Sep 17 00:00:00 2001 From: rambohe Date: Mon, 24 Jul 2023 11:01:19 +0800 Subject: [PATCH 60/93] add kubelet certificate mode in yurthub (#1625) --- cmd/yurthub/app/config/config.go | 57 ++----- cmd/yurthub/app/config/config_test.go | 4 +- cmd/yurthub/app/options/options.go | 3 + cmd/yurthub/app/options/options_test.go | 1 + pkg/yurthub/certificate/interfaces.go | 15 +- .../kubeletcertificate/kubelet_certificate.go | 119 ++++++++++++++ .../kubelet_certificate_test.go | 59 +++++++ pkg/yurthub/certificate/manager/manager.go | 137 ++++++++++++++++ .../certificate/manager/manager_test.go | 136 +++++++++++++++ pkg/yurthub/certificate/server/server.go | 102 ++++++++++++ pkg/yurthub/certificate/server/server_test.go | 55 +++++++ .../certificate/{token => }/testdata/ca.crt | 0 .../certificate/{token => }/testdata/ca.key | 0 .../{token => }/testdata/fake_client.go | 0 pkg/yurthub/certificate/testdata/kubelet.conf | 14 ++ pkg/yurthub/certificate/testdata/kubelet.pem | 22 +++ pkg/yurthub/certificate/token/token.go | 132 +++------------ pkg/yurthub/certificate/token/token_test.go | 155 +++--------------- pkg/yurthub/kubernetes/rest/config_test.go | 18 +- pkg/yurthub/server/certificate_test.go | 19 +-- 20 files changed, 733 insertions(+), 315 deletions(-) create mode 100644 pkg/yurthub/certificate/kubeletcertificate/kubelet_certificate.go create mode 100644 pkg/yurthub/certificate/kubeletcertificate/kubelet_certificate_test.go create mode 100644 pkg/yurthub/certificate/manager/manager.go create mode 100644 pkg/yurthub/certificate/manager/manager_test.go create mode 100644 pkg/yurthub/certificate/server/server.go create mode 100644 pkg/yurthub/certificate/server/server_test.go rename pkg/yurthub/certificate/{token => }/testdata/ca.crt (100%) rename pkg/yurthub/certificate/{token => }/testdata/ca.key (100%) rename pkg/yurthub/certificate/{token => }/testdata/fake_client.go (100%) create mode 100644 pkg/yurthub/certificate/testdata/kubelet.conf create mode 100644 pkg/yurthub/certificate/testdata/kubelet.pem diff --git a/cmd/yurthub/app/config/config.go b/cmd/yurthub/app/config/config.go index 85a64ab435a..06529a69fc5 100644 --- a/cmd/yurthub/app/config/config.go +++ b/cmd/yurthub/app/config/config.go @@ -44,10 +44,9 @@ import ( "github.com/openyurtio/openyurt/cmd/yurthub/app/options" "github.com/openyurtio/openyurt/pkg/projectinfo" - ipUtils "github.com/openyurtio/openyurt/pkg/util/ip" "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/certificate" - "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token" + certificatemgr "github.com/openyurtio/openyurt/pkg/yurthub/certificate/manager" "github.com/openyurtio/openyurt/pkg/yurthub/filter" "github.com/openyurtio/openyurt/pkg/yurthub/filter/manager" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/meta" @@ -178,10 +177,21 @@ func Complete(options *options.YurtHubOptions) (*YurtHubConfiguration, error) { LeaderElection: options.LeaderElection, } - certMgr, err := createCertManager(options, us) + certMgr, err := certificatemgr.NewYurtHubCertManager(options, us) if err != nil { return nil, err } + certMgr.Start() + err = wait.PollImmediate(5*time.Second, 4*time.Minute, func() (bool, error) { + isReady := certMgr.Ready() + if isReady { + return true, nil + } + return false, nil + }) + if err != nil { + return nil, fmt.Errorf("hub certificates preparation failed, %v", err) + } cfg.CertManager = certMgr if options.EnableDummyIf { @@ -230,7 +240,7 @@ func parseRemoteServers(serverAddr string) ([]*url.URL, error) { return us, nil } -// createSharedInformers create sharedInformers from the given proxyAddr. +// createClientAndSharedInformers create kubeclient and sharedInformers from the given proxyAddr. func createClientAndSharedInformers(proxyAddr string, enableNodePool bool) (kubernetes.Interface, informers.SharedInformerFactory, yurtinformers.SharedInformerFactory, error) { var kubeConfig *rest.Config var yurtClient yurtclientset.Interface @@ -341,45 +351,6 @@ func isServiceTopologyFilterEnabled(options *options.YurtHubOptions) bool { return true } -func createCertManager(options *options.YurtHubOptions, remoteServers []*url.URL) (certificate.YurtCertificateManager, error) { - // use dummy ip and bind ip as cert IP SANs - certIPs := ipUtils.RemoveDupIPs([]net.IP{ - net.ParseIP(options.HubAgentDummyIfIP), - net.ParseIP(options.YurtHubHost), - net.ParseIP(options.YurtHubProxyHost), - }) - - cfg := &token.CertificateManagerConfiguration{ - RootDir: options.RootDir, - NodeName: options.NodeName, - JoinToken: options.JoinToken, - BootstrapFile: options.BootstrapFile, - CaCertHashes: options.CACertHashes, - YurtHubCertOrganizations: options.YurtHubCertOrganizations, - CertIPs: certIPs, - RemoteServers: remoteServers, - Client: options.ClientForTest, - } - certManager, err := token.NewYurtHubCertManager(cfg) - if err != nil { - return nil, fmt.Errorf("failed to create cert manager for yurthub, %v", err) - } - - certManager.Start() - err = wait.PollImmediate(5*time.Second, 4*time.Minute, func() (bool, error) { - isReady := certManager.Ready() - if isReady { - return true, nil - } - return false, nil - }) - if err != nil { - return nil, fmt.Errorf("hub certificates preparation failed, %v", err) - } - - return certManager, nil -} - func prepareServerServing(options *options.YurtHubOptions, certMgr certificate.YurtCertificateManager, cfg *YurtHubConfiguration) error { if err := (&apiserveroptions.DeprecatedInsecureServingOptions{ BindAddress: net.ParseIP(options.YurtHubHost), diff --git a/cmd/yurthub/app/config/config_test.go b/cmd/yurthub/app/config/config_test.go index cd657071e51..3fd46d2108e 100644 --- a/cmd/yurthub/app/config/config_test.go +++ b/cmd/yurthub/app/config/config_test.go @@ -20,12 +20,12 @@ import ( "testing" "github.com/openyurtio/openyurt/cmd/yurthub/app/options" - "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token/testdata" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/testdata" ) func TestComplete(t *testing.T) { options := options.NewYurtHubOptions() - client, err := testdata.CreateCertFakeClient("../../../../pkg/yurthub/certificate/token/testdata") + client, err := testdata.CreateCertFakeClient("../../../../pkg/yurthub/certificate/testdata") if err != nil { t.Errorf("failed to create cert fake client, %v", err) return diff --git a/cmd/yurthub/app/options/options.go b/cmd/yurthub/app/options/options.go index ccabdbdd097..a6f0deadbaf 100644 --- a/cmd/yurthub/app/options/options.go +++ b/cmd/yurthub/app/options/options.go @@ -62,6 +62,7 @@ type YurtHubOptions struct { HeartbeatIntervalSeconds int MaxRequestInFlight int JoinToken string + BootstrapMode string BootstrapFile string RootDir string Version bool @@ -105,6 +106,7 @@ func NewYurtHubOptions() *YurtHubOptions { HeartbeatTimeoutSeconds: 2, HeartbeatIntervalSeconds: 10, MaxRequestInFlight: 250, + BootstrapMode: "token", RootDir: filepath.Join("/var/lib/", projectinfo.GetHubName()), EnableProfiling: true, EnableDummyIf: true, @@ -189,6 +191,7 @@ func (o *YurtHubOptions) AddFlags(fs *pflag.FlagSet) { fs.IntVar(&o.MaxRequestInFlight, "max-requests-in-flight", o.MaxRequestInFlight, "the maximum number of parallel requests.") fs.StringVar(&o.JoinToken, "join-token", o.JoinToken, "the Join token for bootstrapping hub agent.") fs.MarkDeprecated("join-token", "It is planned to be removed from OpenYurt in the version v1.5. Please use --bootstrap-file to bootstrap hub agent.") + fs.StringVar(&o.BootstrapMode, "bootstrap-mode", o.BootstrapMode, "the mode for bootstrapping hub agent(token, kubeletcertificate).") fs.StringVar(&o.BootstrapFile, "bootstrap-file", o.BootstrapFile, "the bootstrap file for bootstrapping hub agent.") fs.StringVar(&o.RootDir, "root-dir", o.RootDir, "directory path for managing hub agent files(pki, cache etc).") fs.BoolVar(&o.Version, "version", o.Version, "print the version information.") diff --git a/cmd/yurthub/app/options/options_test.go b/cmd/yurthub/app/options/options_test.go index d9d2379b600..a4370df98cc 100644 --- a/cmd/yurthub/app/options/options_test.go +++ b/cmd/yurthub/app/options/options_test.go @@ -49,6 +49,7 @@ func TestNewYurtHubOptions(t *testing.T) { HeartbeatTimeoutSeconds: 2, HeartbeatIntervalSeconds: 10, MaxRequestInFlight: 250, + BootstrapMode: "token", RootDir: filepath.Join("/var/lib/", projectinfo.GetHubName()), EnableProfiling: true, EnableDummyIf: true, diff --git a/pkg/yurthub/certificate/interfaces.go b/pkg/yurthub/certificate/interfaces.go index 6ac9d8a9bae..ecbaedc8220 100644 --- a/pkg/yurthub/certificate/interfaces.go +++ b/pkg/yurthub/certificate/interfaces.go @@ -22,14 +22,25 @@ import ( // YurtCertificateManager is responsible for managing node certificate for yurthub type YurtCertificateManager interface { - Start() - Stop() + YurtClientCertificateManager + YurtServerCertificateManager // Ready should be called after yurt certificate manager started by Start. Ready() bool +} + +// YurtClientCertificateManager is responsible for managing node client certificates for yurthub +type YurtClientCertificateManager interface { + Start() + Stop() UpdateBootstrapConf(joinToken string) error GetHubConfFile() string GetCaFile() string GetAPIServerClientCert() *tls.Certificate +} + +type YurtServerCertificateManager interface { + Start() + Stop() GetHubServerCert() *tls.Certificate GetHubServerCertFile() string } diff --git a/pkg/yurthub/certificate/kubeletcertificate/kubelet_certificate.go b/pkg/yurthub/certificate/kubeletcertificate/kubelet_certificate.go new file mode 100644 index 00000000000..00d1e7e911d --- /dev/null +++ b/pkg/yurthub/certificate/kubeletcertificate/kubelet_certificate.go @@ -0,0 +1,119 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubeletcertificate + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "time" + + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/yurthub/certificate" + "github.com/openyurtio/openyurt/pkg/yurthub/util" +) + +var ( + KubeConfNotExistErr = errors.New("/etc/kubernetes/kubelet.conf file doesn't exist") + KubeletCANotExistErr = errors.New("/etc/kubernetes/pki/ca.crt file doesn't exist") + KubeletPemNotExistErr = errors.New("/var/lib/kubelet/pki/kubelet-current.pem file doesn't exist") +) + +type kubeletCertManager struct { + kubeConfFile string + kubeletCAFile string + kubeletPemFile string + cert *tls.Certificate +} + +func NewKubeletCertManager(kubeConfFile, kubeletCAFile, kubeletPemFile string) (certificate.YurtClientCertificateManager, error) { + if exist, _ := util.FileExists(kubeConfFile); !exist { + return nil, KubeConfNotExistErr + } + + if exist, _ := util.FileExists(kubeletCAFile); !exist { + return nil, KubeletCANotExistErr + } + + if exist, _ := util.FileExists(kubeletPemFile); !exist { + return nil, KubeletPemNotExistErr + } + + cert, err := loadFile(kubeletPemFile) + if err != nil { + return nil, err + } + + return &kubeletCertManager{ + kubeConfFile: kubeConfFile, + kubeletCAFile: kubeletCAFile, + kubeletPemFile: kubeletPemFile, + cert: cert, + }, nil +} + +func (kcm *kubeletCertManager) Start() { + // do nothing +} + +func (kcm *kubeletCertManager) Stop() { + // do nothing +} + +func (kcm *kubeletCertManager) UpdateBootstrapConf(_ string) error { + return nil +} + +func (kcm *kubeletCertManager) GetHubConfFile() string { + return kcm.kubeConfFile +} + +func (kcm *kubeletCertManager) GetCaFile() string { + return kcm.kubeletCAFile +} + +func (kcm *kubeletCertManager) GetAPIServerClientCert() *tls.Certificate { + if kcm.cert != nil && kcm.cert.Leaf != nil && !time.Now().After(kcm.cert.Leaf.NotAfter) { + return kcm.cert + } + + klog.Warningf("current certificate: %s is expired, reload it", kcm.kubeletPemFile) + cert, err := loadFile(kcm.kubeletPemFile) + if err != nil { + klog.Errorf("failed to load client certificate(%s), %v", kcm.kubeletPemFile, err) + return nil + } + kcm.cert = cert + return kcm.cert +} + +func loadFile(pairFile string) (*tls.Certificate, error) { + // LoadX509KeyPair knows how to parse combined cert and private key from + // the same file. + cert, err := tls.LoadX509KeyPair(pairFile, pairFile) + if err != nil { + return nil, fmt.Errorf("could not convert data from %q into cert/key pair: %v", pairFile, err) + } + certs, err := x509.ParseCertificates(cert.Certificate[0]) + if err != nil { + return nil, fmt.Errorf("unable to parse certificate data: %v", err) + } + cert.Leaf = certs[0] + return &cert, nil +} diff --git a/pkg/yurthub/certificate/kubeletcertificate/kubelet_certificate_test.go b/pkg/yurthub/certificate/kubeletcertificate/kubelet_certificate_test.go new file mode 100644 index 00000000000..c9e46a72506 --- /dev/null +++ b/pkg/yurthub/certificate/kubeletcertificate/kubelet_certificate_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubeletcertificate + +import "testing" + +func TestNewKubeletCertManager(t *testing.T) { + testcases := map[string]struct { + kubeConfFile string + kubeletCAFile string + kubeletPemFile string + err error + }{ + "kubelet.conf doesn't exist": { + kubeConfFile: "invalid file", + err: KubeConfNotExistErr, + }, + "ca.crt file doesn't exist": { + kubeConfFile: "../testdata/kubelet.conf", + kubeletCAFile: "invalid file", + err: KubeletCANotExistErr, + }, + "kubelet.pem doesn't exist": { + kubeConfFile: "../testdata/kubelet.conf", + kubeletCAFile: "../testdata/ca.crt", + kubeletPemFile: "invalid file", + err: KubeletPemNotExistErr, + }, + "normal kubelet cert manager": { + kubeConfFile: "../testdata/kubelet.conf", + kubeletCAFile: "../testdata/ca.crt", + kubeletPemFile: "../testdata/kubelet.pem", + err: nil, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + _, err := NewKubeletCertManager(tc.kubeConfFile, tc.kubeletCAFile, tc.kubeletPemFile) + if err != tc.err { + t.Errorf("expect error is %v, but got %v", tc.err, err) + } + }) + } +} diff --git a/pkg/yurthub/certificate/manager/manager.go b/pkg/yurthub/certificate/manager/manager.go new file mode 100644 index 00000000000..9d1f8cfff40 --- /dev/null +++ b/pkg/yurthub/certificate/manager/manager.go @@ -0,0 +1,137 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manager + +import ( + "errors" + "net" + "net/url" + "path/filepath" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/cmd/yurthub/app/options" + "github.com/openyurtio/openyurt/pkg/projectinfo" + ipUtils "github.com/openyurtio/openyurt/pkg/util/ip" + hubCert "github.com/openyurtio/openyurt/pkg/yurthub/certificate" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/kubeletcertificate" + hubServerCert "github.com/openyurtio/openyurt/pkg/yurthub/certificate/server" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token" + "github.com/openyurtio/openyurt/pkg/yurthub/util" +) + +const ( + KubeConfFile = "/etc/kubernetes/kubelet.conf" + KubeletCAFile = "/etc/kubernetes/pki/ca.crt" + KubeletPemFile = "/var/lib/kubelet/pki/current-kubelet.pem" +) + +var ( + serverCertNotReadyError = errors.New("hub server certificate") + apiServerClientCertNotReadyError = errors.New("APIServer client certificate") + caCertIsNotReadyError = errors.New("ca.crt file") + + DefaultRootDir = "/var/lib" +) + +type yurtHubCertManager struct { + hubCert.YurtClientCertificateManager + hubCert.YurtServerCertificateManager +} + +// NewYurtHubCertManager new a YurtCertificateManager instance +func NewYurtHubCertManager(options *options.YurtHubOptions, remoteServers []*url.URL) (hubCert.YurtCertificateManager, error) { + var clientCertManager hubCert.YurtClientCertificateManager + var err error + + workDir := filepath.Join(options.RootDir, projectinfo.GetHubName()) + if len(options.RootDir) == 0 { + workDir = filepath.Join(DefaultRootDir, projectinfo.GetHubName()) + } + + if options.BootstrapMode == "kubeletcertificate" { + clientCertManager, err = kubeletcertificate.NewKubeletCertManager(KubeConfFile, KubeletCAFile, KubeletPemFile) + if err != nil { + return nil, err + } + } else { + cfg := &token.ClientCertificateManagerConfiguration{ + WorkDir: workDir, + NodeName: options.NodeName, + JoinToken: options.JoinToken, + BootstrapFile: options.BootstrapFile, + CaCertHashes: options.CACertHashes, + YurtHubCertOrganizations: options.YurtHubCertOrganizations, + RemoteServers: remoteServers, + Client: options.ClientForTest, + } + clientCertManager, err = token.NewYurtHubClientCertManager(cfg) + if err != nil { + return nil, err + } + } + + // use dummy ip and bind ip as cert IP SANs + certIPs := ipUtils.RemoveDupIPs([]net.IP{ + net.ParseIP(options.HubAgentDummyIfIP), + net.ParseIP(options.YurtHubHost), + net.ParseIP(options.YurtHubProxyHost), + }) + serverCertManager, err := hubServerCert.NewHubServerCertificateManager(options.ClientForTest, clientCertManager, options.NodeName, filepath.Join(workDir, "pki"), certIPs) + if err != nil { + return nil, err + } + + hubCertManager := &yurtHubCertManager{ + YurtClientCertificateManager: clientCertManager, + YurtServerCertificateManager: serverCertManager, + } + + return hubCertManager, nil +} + +func (hcm *yurtHubCertManager) Start() { + hcm.YurtClientCertificateManager.Start() + hcm.YurtServerCertificateManager.Start() +} + +func (hcm *yurtHubCertManager) Stop() { + hcm.YurtClientCertificateManager.Stop() + hcm.YurtServerCertificateManager.Stop() +} + +func (hcm *yurtHubCertManager) Ready() bool { + var errs []error + if hcm.GetAPIServerClientCert() == nil { + errs = append(errs, apiServerClientCertNotReadyError) + } + + if exist, _ := util.FileExists(hcm.YurtClientCertificateManager.GetCaFile()); !exist { + errs = append(errs, caCertIsNotReadyError) + } + + if hcm.GetHubServerCert() == nil { + errs = append(errs, serverCertNotReadyError) + } + + if len(errs) != 0 { + klog.Errorf("hub certificates are not ready: %s", utilerrors.NewAggregate(errs).Error()) + return false + } + return true +} diff --git a/pkg/yurthub/certificate/manager/manager_test.go b/pkg/yurthub/certificate/manager/manager_test.go new file mode 100644 index 00000000000..5a462c2fce4 --- /dev/null +++ b/pkg/yurthub/certificate/manager/manager_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manager + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/openyurtio/openyurt/cmd/yurthub/app/options" + "github.com/openyurtio/openyurt/pkg/projectinfo" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/testdata" +) + +func TestGetHubServerCertFile(t *testing.T) { + nodeName := "foo" + u, _ := url.Parse("http://127.0.0.1") + remoteServers := []*url.URL{u} + testcases := map[string]struct { + rootDir string + path string + }{ + "use default root dir": { + rootDir: "", + path: filepath.Join("/var/lib", projectinfo.GetHubName(), "pki", fmt.Sprintf("%s-server-current.pem", projectinfo.GetHubName())), + }, + "define root dir": { + rootDir: "/tmp", + path: filepath.Join("/tmp", projectinfo.GetHubName(), "pki", fmt.Sprintf("%s-server-current.pem", projectinfo.GetHubName())), + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + opt := &options.YurtHubOptions{ + NodeName: nodeName, + YurtHubHost: "127.0.0.1", + RootDir: tc.rootDir, + } + + mgr, err := NewYurtHubCertManager(opt, remoteServers) + if err != nil { + t.Errorf("failed to new cert manager, %v", err) + } + + if mgr.GetHubServerCertFile() != tc.path { + t.Errorf("expect hub server cert file %s, but got %s", tc.path, mgr.GetHubServerCertFile()) + } + }) + } +} + +var ( + joinToken = "123456.abcdef1234567890" + rootDir = "/tmp/token/cert" +) + +func TestReady(t *testing.T) { + nodeName := "foo" + u, _ := url.Parse("http://127.0.0.1") + remoteServers := []*url.URL{u} + + client, err := testdata.CreateCertFakeClient("../testdata") + if err != nil { + t.Errorf("failed to create cert fake client, %v", err) + return + } + + mgr, err := NewYurtHubCertManager(&options.YurtHubOptions{ + NodeName: nodeName, + YurtHubHost: "127.0.0.1", + RootDir: rootDir, + JoinToken: joinToken, + YurtHubCertOrganizations: []string{"yurthub:tenant:foo"}, + ClientForTest: client, + }, remoteServers) + if err != nil { + t.Errorf("failed to new yurt cert manager, %v", err) + return + } + mgr.Start() + + err = wait.PollImmediate(2*time.Second, 1*time.Minute, func() (done bool, err error) { + if mgr.Ready() { + return true, nil + } + return false, nil + }) + + if err != nil { + t.Errorf("certificates are not ready, %v", err) + } + + mgr.Stop() + + // reuse the config and ca file + t.Logf("go to check the reuse of config and ca file") + newMgr, err := NewYurtHubCertManager(&options.YurtHubOptions{ + NodeName: nodeName, + YurtHubHost: "127.0.0.1", + RootDir: rootDir, + JoinToken: joinToken, + YurtHubCertOrganizations: []string{"yurthub:tenant:foo"}, + ClientForTest: client, + }, remoteServers) + if err != nil { + t.Errorf("failed to new another yurt cert manager, %v", err) + return + } + newMgr.Start() + if !newMgr.Ready() { + t.Errorf("certificates can not be reused") + } + newMgr.Stop() + + os.RemoveAll(rootDir) +} diff --git a/pkg/yurthub/certificate/server/server.go b/pkg/yurthub/certificate/server/server.go new file mode 100644 index 00000000000..eae2f136e9a --- /dev/null +++ b/pkg/yurthub/certificate/server/server.go @@ -0,0 +1,102 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "crypto/tls" + "fmt" + "net" + "time" + + "github.com/pkg/errors" + certificatesv1 "k8s.io/api/certificates/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/user" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/util/certificate" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/projectinfo" + yurtutil "github.com/openyurtio/openyurt/pkg/util" + certfactory "github.com/openyurtio/openyurt/pkg/util/certmanager/factory" + "github.com/openyurtio/openyurt/pkg/util/certmanager/store" + kubeconfigutil "github.com/openyurtio/openyurt/pkg/util/kubeconfig" + hubCert "github.com/openyurtio/openyurt/pkg/yurthub/certificate" +) + +type hubServerCertificateManager struct { + hubServerCertManager certificate.Manager + hubServerCertStore certificate.FileStore +} + +func NewHubServerCertificateManager(client clientset.Interface, clientCertManager hubCert.YurtClientCertificateManager, nodeName, pkiDir string, certIPs []net.IP) (hubCert.YurtServerCertificateManager, error) { + hubServerCertStore, err := store.NewFileStoreWrapper(fmt.Sprintf("%s-server", projectinfo.GetHubName()), pkiDir, pkiDir, "", "") + if err != nil { + return nil, errors.Wrap(err, "couldn't new hub server cert store") + } + + kubeClientFn := func(current *tls.Certificate) (clientset.Interface, error) { + // waiting for the certificate is generated + _ = wait.PollInfinite(5*time.Second, func() (bool, error) { + // keep polling until the yurthub client certificate is signed + if clientCertManager.GetAPIServerClientCert() != nil { + return true, nil + } + klog.Infof("waiting for the controller-manager to sign the %s client certificate", projectinfo.GetHubName()) + return false, nil + }) + + if !yurtutil.IsNil(client) { + return client, nil + } + + return kubeconfigutil.ClientSetFromFile(clientCertManager.GetHubConfFile()) + } + + hubServerCertManager, sErr := certfactory.NewCertManagerFactoryWithFnAndStore(kubeClientFn, hubServerCertStore).New(&certfactory.CertManagerConfig{ + ComponentName: fmt.Sprintf("%s-server", projectinfo.GetHubName()), + SignerName: certificatesv1.KubeletServingSignerName, + ForServerUsage: true, + CommonName: fmt.Sprintf("system:node:%s", nodeName), + Organizations: []string{user.NodesGroup}, + IPs: certIPs, + }) + if sErr != nil { + return nil, sErr + } + + return &hubServerCertificateManager{ + hubServerCertManager: hubServerCertManager, + hubServerCertStore: hubServerCertStore, + }, nil +} + +func (hcm *hubServerCertificateManager) Start() { + hcm.hubServerCertManager.Start() +} + +func (hcm *hubServerCertificateManager) Stop() { + hcm.hubServerCertManager.Stop() +} + +func (hcm *hubServerCertificateManager) GetHubServerCert() *tls.Certificate { + return hcm.hubServerCertManager.Current() +} + +func (hcm *hubServerCertificateManager) GetHubServerCertFile() string { + return hcm.hubServerCertStore.CurrentPath() +} diff --git a/pkg/yurthub/certificate/server/server_test.go b/pkg/yurthub/certificate/server/server_test.go new file mode 100644 index 00000000000..eaa4257f787 --- /dev/null +++ b/pkg/yurthub/certificate/server/server_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package server + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/openyurtio/openyurt/pkg/projectinfo" +) + +func TestGetHubServerCertFile(t *testing.T) { + nodeName := "foo" + testcases := map[string]struct { + rootDir string + path string + }{ + "use default root dir": { + rootDir: filepath.Join("/var/lib", projectinfo.GetHubName(), "pki"), + path: filepath.Join("/var/lib", projectinfo.GetHubName(), "pki", fmt.Sprintf("%s-server-current.pem", projectinfo.GetHubName())), + }, + "define root dir": { + rootDir: "/tmp/pki", + path: filepath.Join("/tmp", "pki", fmt.Sprintf("%s-server-current.pem", projectinfo.GetHubName())), + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + mgr, err := NewHubServerCertificateManager(nil, nil, nodeName, tc.rootDir, nil) + if err != nil { + t.Errorf("failed to new cert manager, %v", err) + } + + if mgr.GetHubServerCertFile() != tc.path { + t.Errorf("expect hub server cert file %s, but got %s", tc.path, mgr.GetHubServerCertFile()) + } + }) + } +} diff --git a/pkg/yurthub/certificate/token/testdata/ca.crt b/pkg/yurthub/certificate/testdata/ca.crt similarity index 100% rename from pkg/yurthub/certificate/token/testdata/ca.crt rename to pkg/yurthub/certificate/testdata/ca.crt diff --git a/pkg/yurthub/certificate/token/testdata/ca.key b/pkg/yurthub/certificate/testdata/ca.key similarity index 100% rename from pkg/yurthub/certificate/token/testdata/ca.key rename to pkg/yurthub/certificate/testdata/ca.key diff --git a/pkg/yurthub/certificate/token/testdata/fake_client.go b/pkg/yurthub/certificate/testdata/fake_client.go similarity index 100% rename from pkg/yurthub/certificate/token/testdata/fake_client.go rename to pkg/yurthub/certificate/testdata/fake_client.go diff --git a/pkg/yurthub/certificate/testdata/kubelet.conf b/pkg/yurthub/certificate/testdata/kubelet.conf new file mode 100644 index 00000000000..81f572fffa5 --- /dev/null +++ b/pkg/yurthub/certificate/testdata/kubelet.conf @@ -0,0 +1,14 @@ +apiVersion: v1 +clusters: +- cluster: + server: http://127.0.0.1:10261 + name: default-cluster +contexts: +- context: + cluster: default-cluster + namespace: default + user: default-auth + name: default-context +current-context: default-context +kind: Config +preferences: {} \ No newline at end of file diff --git a/pkg/yurthub/certificate/testdata/kubelet.pem b/pkg/yurthub/certificate/testdata/kubelet.pem new file mode 100644 index 00000000000..adb600c9a01 --- /dev/null +++ b/pkg/yurthub/certificate/testdata/kubelet.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIICtDCCAZygAwIBAgIRAO5qKHQa5BXX7iEe6CWQ3pAwDQYJKoZIhvcNAQELBQAw +PjEnMA8GA1UEChMIaGFuZ3pob3UwFAYDVQQKEw1hbGliYWJhIGNsb3VkMRMwEQYD +VQQDEwprdWJlcm5ldGVzMB4XDTIzMDcxOTA2MzMwOFoXDTMzMDcxNjA2MzMwOFow +STEVMBMGA1UEChMMc3lzdGVtOm5vZGVzMTAwLgYDVQQDEydzeXN0ZW06bm9kZTpp +LTV5aHE3YnIwZG5rdjgwcXZmM3hyZnUzYXAwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAARyoup5dNsDp+GOT0nNyowfSp85coVhJ275rqrZOIHIBlhvzJCezK1PVe4r +J9QzzJC03pwl6xoFsFvfI6UG8+G1o20wazAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0l +BAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBQtE/d8pSus +mZzIdzVsFkKmxhhvpjAVBgNVHREEDjAMhwSp/gIBhwR/AAABMA0GCSqGSIb3DQEB +CwUAA4IBAQAoRPEFz1mPq/UzLzSMvxIbmz+FiPH3kuX3/j3FAN+kVelz7MeW/L5/ +HvoxfXWXKm+C0XczNj8Oo2GayNCh4VnHdoWIE2d4XMxZsH1PCJjYHTthJ6WQD1b+ +29VxBQXSthx1WumYkCMDWEduTnTsN3jAbayYBAAWvz+qgBn1Lb/HpJofSCnrZ5je +n596LOHS0UXZDyO5aNVXq0+hydtk/KdR33iA1Tp3X16wqM4C7xeKHixwbYucuUyV +/0P5S0UPp9V03V86xe9p0VHm//1CIS00/wuOh7ituWWX7CU68ZyxyGirAr8r4ojF +wqhGDLZ1ek34ufoADQut/XIhpdeYjNq7 +-----END CERTIFICATE----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJ5xeWx4KNT2gjD4GgWATD19ZSJ00BHQHO61vqYziWm7oAoGCCqGSM49 +AwEHoUQDQgAEcqLqeXTbA6fhjk9JzcqMH0qfOXKFYSdu+a6q2TiByAZYb8yQnsyt +T1XuKyfUM8yQtN6cJesaBbBb3yOlBvPhtQ== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/yurthub/certificate/token/token.go b/pkg/yurthub/certificate/token/token.go index 6ce163ce2cc..459470e5b41 100644 --- a/pkg/yurthub/certificate/token/token.go +++ b/pkg/yurthub/certificate/token/token.go @@ -28,8 +28,6 @@ import ( "github.com/pkg/errors" certificatesv1 "k8s.io/api/certificates/v1" - utilerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/user" clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" @@ -51,39 +49,32 @@ import ( const ( YurtHubCSROrg = "openyurt:yurthub" - DefaultRootDir = "/var/lib" hubPkiDirName = "pki" hubCaFileName = "ca.crt" bootstrapConfigFileName = "bootstrap-hub.conf" ) var ( - hubConfigFileName = fmt.Sprintf("%s.conf", projectinfo.GetHubName()) - serverCertNotReadyError = errors.New("hub server certificate") - apiServerClientCertNotReadyError = errors.New("APIServer client certificate") - caCertIsNotReadyError = errors.New("ca.crt file") + hubConfigFileName = fmt.Sprintf("%s.conf", projectinfo.GetHubName()) ) -type CertificateManagerConfiguration struct { - RootDir string +type ClientCertificateManagerConfiguration struct { + WorkDir string NodeName string JoinToken string BootstrapFile string CaCertHashes []string YurtHubCertOrganizations []string - CertIPs []net.IP RemoteServers []*url.URL Client clientset.Interface } -type yurtHubCertManager struct { +type yurtHubClientCertManager struct { client clientset.Interface remoteServers []*url.URL caCertHashes []string apiServerClientCertManager certificate.Manager - hubServerCertManager certificate.Manager apiServerClientCertStore certificate.FileStore - hubServerCertStore certificate.FileStore hubRunDir string hubName string joinToken string @@ -91,19 +82,13 @@ type yurtHubCertManager struct { dialer *util.Dialer } -// NewYurtHubCertManager new a YurtCertificateManager instance -func NewYurtHubCertManager(cfg *CertificateManagerConfiguration) (hubCert.YurtCertificateManager, error) { +// NewYurtHubClientCertManager new a YurtCertificateManager instance +func NewYurtHubClientCertManager(cfg *ClientCertificateManagerConfiguration) (hubCert.YurtClientCertificateManager, error) { var err error - - hubRunDir := cfg.RootDir - if len(cfg.RootDir) == 0 { - hubRunDir = filepath.Join(DefaultRootDir, projectinfo.GetHubName()) - } - - ycm := &yurtHubCertManager{ + ycm := &yurtHubClientCertManager{ client: cfg.Client, remoteServers: cfg.RemoteServers, - hubRunDir: hubRunDir, + hubRunDir: cfg.WorkDir, hubName: projectinfo.GetHubName(), joinToken: cfg.JoinToken, bootstrapFile: cfg.BootstrapFile, @@ -124,17 +109,6 @@ func NewYurtHubCertManager(cfg *CertificateManagerConfiguration) (hubCert.YurtCe return ycm, errors.Wrap(err, "couldn't new apiserver client certificate manager") } - // 3. prepare yurthub server certificate manager - ycm.hubServerCertStore, err = store.NewFileStoreWrapper(fmt.Sprintf("%s-server", ycm.hubName), ycm.getPkiDir(), ycm.getPkiDir(), "", "") - if err != nil { - return ycm, errors.Wrap(err, "couldn't new hub server cert store") - } - - ycm.hubServerCertManager, err = ycm.newHubServerCertificateManager(ycm.hubServerCertStore, cfg.NodeName, cfg.CertIPs) - if err != nil { - return ycm, errors.Wrap(err, "couldn't new hub server certificate manager") - } - return ycm, nil } @@ -152,7 +126,7 @@ func removeDirContents(dir string) error { return nil } -func (ycm *yurtHubCertManager) verifyServerAddrOrCleanup(servers []*url.URL) { +func (ycm *yurtHubClientCertManager) verifyServerAddrOrCleanup(servers []*url.URL) { if cfg, err := clientcmd.LoadFromFile(ycm.GetHubConfFile()); err == nil { cluster := kubeconfigutil.GetClusterFromKubeConfig(cfg) if serverURL, err := url.Parse(cluster.Server); err != nil { @@ -172,7 +146,7 @@ func (ycm *yurtHubCertManager) verifyServerAddrOrCleanup(servers []*url.URL) { } // Start init certificate manager and certs for hub agent -func (ycm *yurtHubCertManager) Start() { +func (ycm *yurtHubClientCertManager) Start() { err := ycm.prepareConfigAndCaFile() if err != nil { klog.Errorf("failed to prepare config and ca file, %v", err) @@ -180,7 +154,6 @@ func (ycm *yurtHubCertManager) Start() { } ycm.apiServerClientCertManager.Start() - ycm.hubServerCertManager.Start() } // prepareConfigAndCaFile is used to create the following three files. @@ -188,7 +161,7 @@ func (ycm *yurtHubCertManager) Start() { // - /var/lib/yurthub/yurthub.conf // - /var/lib/yurthub/pki/ca.crt // if these files already exist, just reuse them. -func (ycm *yurtHubCertManager) prepareConfigAndCaFile() error { +func (ycm *yurtHubClientCertManager) prepareConfigAndCaFile() error { var tlsBootstrapCfg *clientcmdapi.Config var hubKubeConfig *clientcmdapi.Config var err error @@ -288,46 +261,23 @@ func (ycm *yurtHubCertManager) prepareConfigAndCaFile() error { } // Stop the cert manager loop -func (ycm *yurtHubCertManager) Stop() { +func (ycm *yurtHubClientCertManager) Stop() { ycm.apiServerClientCertManager.Stop() - ycm.hubServerCertManager.Stop() -} - -// Ready is used for checking client/server/ca certificates are prepared completely or not. -func (ycm *yurtHubCertManager) Ready() bool { - var errs []error - if ycm.GetHubServerCert() == nil { - errs = append(errs, serverCertNotReadyError) - } - - if ycm.GetAPIServerClientCert() == nil { - errs = append(errs, apiServerClientCertNotReadyError) - } - - if exist, _ := util.FileExists(ycm.GetCaFile()); !exist { - errs = append(errs, caCertIsNotReadyError) - } - - if len(errs) != 0 { - klog.Errorf("hub certificates are not ready: %s", utilerrors.NewAggregate(errs).Error()) - return false - } - return true } // UpdateBootstrapConf is used for revising bootstrap conf file by new bearer token. -func (ycm *yurtHubCertManager) UpdateBootstrapConf(joinToken string) error { +func (ycm *yurtHubClientCertManager) UpdateBootstrapConf(joinToken string) error { _, err := ycm.retrieveHubBootstrapConfig(joinToken) return err } // getPkiDir returns the directory for storing hub agent pki -func (ycm *yurtHubCertManager) getPkiDir() string { +func (ycm *yurtHubClientCertManager) getPkiDir() string { return filepath.Join(ycm.hubRunDir, hubPkiDirName) } // getBootstrapConfFile returns the path of yurthub bootstrap conf file -func (ycm *yurtHubCertManager) getBootstrapConfFile() string { +func (ycm *yurtHubClientCertManager) getBootstrapConfFile() string { if len(ycm.bootstrapFile) != 0 { return ycm.bootstrapFile } @@ -335,30 +285,22 @@ func (ycm *yurtHubCertManager) getBootstrapConfFile() string { } // GetCaFile returns the path of ca file -func (ycm *yurtHubCertManager) GetCaFile() string { +func (ycm *yurtHubClientCertManager) GetCaFile() string { return filepath.Join(ycm.getPkiDir(), hubCaFileName) } // GetHubConfFile returns the path of yurtHub config file path -func (ycm *yurtHubCertManager) GetHubConfFile() string { +func (ycm *yurtHubClientCertManager) GetHubConfFile() string { return filepath.Join(ycm.hubRunDir, hubConfigFileName) } -func (ycm *yurtHubCertManager) GetAPIServerClientCert() *tls.Certificate { +func (ycm *yurtHubClientCertManager) GetAPIServerClientCert() *tls.Certificate { return ycm.apiServerClientCertManager.Current() } -func (ycm *yurtHubCertManager) GetHubServerCert() *tls.Certificate { - return ycm.hubServerCertManager.Current() -} - -func (ycm *yurtHubCertManager) GetHubServerCertFile() string { - return ycm.hubServerCertStore.CurrentPath() -} - // newAPIServerClientCertificateManager create a certificate manager for yurthub component to prepare client certificate // that used to proxy requests to remote kube-apiserver. -func (ycm *yurtHubCertManager) newAPIServerClientCertificateManager(fileStore certificate.FileStore, nodeName string, hubCertOrganizations []string) (certificate.Manager, error) { +func (ycm *yurtHubClientCertManager) newAPIServerClientCertificateManager(fileStore certificate.FileStore, nodeName string, hubCertOrganizations []string) (certificate.Manager, error) { orgs := []string{YurtHubCSROrg, user.NodesGroup} for _, v := range hubCertOrganizations { if v != YurtHubCSROrg && v != user.NodesGroup { @@ -374,7 +316,7 @@ func (ycm *yurtHubCertManager) newAPIServerClientCertificateManager(fileStore ce }) } -func (ycm *yurtHubCertManager) generateCertClientFn(current *tls.Certificate) (clientset.Interface, error) { +func (ycm *yurtHubClientCertManager) generateCertClientFn(current *tls.Certificate) (clientset.Interface, error) { var kubeconfig *restclient.Config var err error if !yurtutil.IsNil(ycm.client) { @@ -412,39 +354,7 @@ func (ycm *yurtHubCertManager) generateCertClientFn(current *tls.Certificate) (c return clientset.NewForConfig(kubeconfig) } -// newHubServerCertificateManager create a certificate manager for yurthub component to prepare server certificate -// that used to handle requests from clients on edge nodes. -func (ycm *yurtHubCertManager) newHubServerCertificateManager(fileStore certificate.FileStore, nodeName string, certIPs []net.IP) (certificate.Manager, error) { - kubeClientFn := func(current *tls.Certificate) (clientset.Interface, error) { - // waiting for the certificate is generated - _ = wait.PollInfinite(5*time.Second, func() (bool, error) { - // keep polling until the yurthub client certificate is signed - if ycm.apiServerClientCertManager.Current() != nil { - return true, nil - } - klog.Infof("waiting for the controller-manager to sign the %s client certificate", ycm.hubName) - return false, nil - }) - - if !yurtutil.IsNil(ycm.client) { - return ycm.client, nil - } - - return kubeconfigutil.ClientSetFromFile(ycm.GetHubConfFile()) - } - - // create a certificate manager for the yurthub server and run the csr approver for both yurthub - return certfactory.NewCertManagerFactoryWithFnAndStore(kubeClientFn, fileStore).New(&certfactory.CertManagerConfig{ - ComponentName: fmt.Sprintf("%s-server", ycm.hubName), - SignerName: certificatesv1.KubeletServingSignerName, - ForServerUsage: true, - CommonName: fmt.Sprintf("system:node:%s", nodeName), - Organizations: []string{user.NodesGroup}, - IPs: certIPs, - }) -} - -func (ycm *yurtHubCertManager) retrieveHubBootstrapConfig(joinToken string) (*clientcmdapi.Config, error) { +func (ycm *yurtHubClientCertManager) retrieveHubBootstrapConfig(joinToken string) (*clientcmdapi.Config, error) { // retrieve bootstrap config info from cluster-info configmap by bootstrap token serverAddr := findActiveRemoteServer(ycm.remoteServers).Host if cfg, err := token.RetrieveValidatedConfigInfo(ycm.client, &token.BootstrapData{ diff --git a/pkg/yurthub/certificate/token/token_test.go b/pkg/yurthub/certificate/token/token_test.go index eebc1856340..8a9d07a1ae4 100644 --- a/pkg/yurthub/certificate/token/token_test.go +++ b/pkg/yurthub/certificate/token/token_test.go @@ -18,17 +18,13 @@ package token import ( "fmt" - "net" "net/url" "os" "path/filepath" "testing" - "time" - - "k8s.io/apimachinery/pkg/util/wait" "github.com/openyurtio/openyurt/pkg/projectinfo" - "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token/testdata" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/testdata" ) func Test_removeDirContents(t *testing.T) { @@ -78,31 +74,29 @@ func TestGetHubConfFile(t *testing.T) { nodeName := "foo" u, _ := url.Parse("http://127.0.0.1") remoteServers := []*url.URL{u} - certIPs := []net.IP{net.ParseIP("127.0.0.1")} testcases := map[string]struct { - rootDir string + workDir string path string }{ "use default root dir": { - rootDir: "", + workDir: filepath.Join("/var/lib", projectinfo.GetHubName()), path: filepath.Join("/var/lib", projectinfo.GetHubName(), fmt.Sprintf("%s.conf", projectinfo.GetHubName())), }, "define root dir": { - rootDir: "/tmp", + workDir: "/tmp", path: filepath.Join("/tmp", fmt.Sprintf("%s.conf", projectinfo.GetHubName())), }, } for k, tc := range testcases { t.Run(k, func(t *testing.T) { - cfg := &CertificateManagerConfiguration{ + cfg := &ClientCertificateManagerConfiguration{ NodeName: nodeName, RemoteServers: remoteServers, - CertIPs: certIPs, - RootDir: tc.rootDir, + WorkDir: tc.workDir, } - mgr, err := NewYurtHubCertManager(cfg) + mgr, err := NewYurtHubClientCertManager(cfg) if err != nil { t.Errorf("failed to new cert manager, %v", err) } @@ -118,31 +112,29 @@ func TestGetCaFile(t *testing.T) { nodeName := "foo" u, _ := url.Parse("http://127.0.0.1") remoteServers := []*url.URL{u} - certIPs := []net.IP{net.ParseIP("127.0.0.1")} testcases := map[string]struct { - rootDir string + workDir string path string }{ "use default root dir": { - rootDir: "", + workDir: filepath.Join("/var/lib", projectinfo.GetHubName()), path: filepath.Join("/var/lib", projectinfo.GetHubName(), "pki", "ca.crt"), }, "define root dir": { - rootDir: "/tmp", + workDir: "/tmp", path: filepath.Join("/tmp", "pki", "ca.crt"), }, } for k, tc := range testcases { t.Run(k, func(t *testing.T) { - cfg := &CertificateManagerConfiguration{ + cfg := &ClientCertificateManagerConfiguration{ NodeName: nodeName, RemoteServers: remoteServers, - CertIPs: certIPs, - RootDir: tc.rootDir, + WorkDir: tc.workDir, } - mgr, err := NewYurtHubCertManager(cfg) + mgr, err := NewYurtHubClientCertManager(cfg) if err != nil { t.Errorf("failed to new cert manager, %v", err) } @@ -154,56 +146,12 @@ func TestGetCaFile(t *testing.T) { } } -func TestGetHubServerCertFile(t *testing.T) { - nodeName := "foo" - u, _ := url.Parse("http://127.0.0.1") - remoteServers := []*url.URL{u} - certIPs := []net.IP{net.ParseIP("127.0.0.1")} - testcases := map[string]struct { - rootDir string - path string - }{ - "use default root dir": { - rootDir: "", - path: filepath.Join("/var/lib", projectinfo.GetHubName(), "pki", fmt.Sprintf("%s-server-current.pem", projectinfo.GetHubName())), - }, - "define root dir": { - rootDir: "/tmp", - path: filepath.Join("/tmp", "pki", fmt.Sprintf("%s-server-current.pem", projectinfo.GetHubName())), - }, - } - - for k, tc := range testcases { - t.Run(k, func(t *testing.T) { - cfg := &CertificateManagerConfiguration{ - NodeName: nodeName, - RemoteServers: remoteServers, - CertIPs: certIPs, - RootDir: tc.rootDir, - } - - mgr, err := NewYurtHubCertManager(cfg) - if err != nil { - t.Errorf("failed to new cert manager, %v", err) - } - - if mgr.GetHubServerCertFile() != tc.path { - t.Errorf("expect hub server cert file %s, but got %s", tc.path, mgr.GetHubServerCertFile()) - } - }) - } -} - -var ( - joinToken = "123456.abcdef1234567890" - rootDir = "/tmp/token/cert" -) - func TestUpdateBootstrapConf(t *testing.T) { + joinToken := "123456.abcdef1234567890" + workDir := "/tmp/token/cert" nodeName := "foo" u, _ := url.Parse("http://127.0.0.1") remoteServers := []*url.URL{u} - certIPs := []net.IP{net.ParseIP("127.0.0.1")} testcases := map[string]struct { joinToken string err error @@ -216,17 +164,16 @@ func TestUpdateBootstrapConf(t *testing.T) { for k, tc := range testcases { t.Run(k, func(t *testing.T) { - client, err := testdata.CreateCertFakeClient("./testdata") + client, err := testdata.CreateCertFakeClient("../testdata") if err != nil { t.Errorf("failed to create cert fake client, %v", err) return } - mgr, err := NewYurtHubCertManager(&CertificateManagerConfiguration{ + mgr, err := NewYurtHubClientCertManager(&ClientCertificateManagerConfiguration{ NodeName: nodeName, RemoteServers: remoteServers, - CertIPs: certIPs, - RootDir: rootDir, + WorkDir: workDir, JoinToken: tc.joinToken, Client: client, }) @@ -242,69 +189,5 @@ func TestUpdateBootstrapConf(t *testing.T) { mgr.Stop() }) } - os.RemoveAll(rootDir) -} - -func TestReady(t *testing.T) { - nodeName := "foo" - u, _ := url.Parse("http://127.0.0.1") - remoteServers := []*url.URL{u} - certIPs := []net.IP{net.ParseIP("127.0.0.1")} - - client, err := testdata.CreateCertFakeClient("./testdata") - if err != nil { - t.Errorf("failed to create cert fake client, %v", err) - return - } - - mgr, err := NewYurtHubCertManager(&CertificateManagerConfiguration{ - NodeName: nodeName, - RemoteServers: remoteServers, - CertIPs: certIPs, - RootDir: rootDir, - JoinToken: joinToken, - YurtHubCertOrganizations: []string{"yurthub:tenant:foo"}, - Client: client, - }) - if err != nil { - t.Errorf("failed to new yurt cert manager, %v", err) - return - } - mgr.Start() - - err = wait.PollImmediate(2*time.Second, 1*time.Minute, func() (done bool, err error) { - if mgr.Ready() { - return true, nil - } - return false, nil - }) - - if err != nil { - t.Errorf("certificates are not ready, %v", err) - } - - mgr.Stop() - - // reuse the config and ca file - t.Logf("go to check the reuse of config and ca file") - newMgr, err := NewYurtHubCertManager(&CertificateManagerConfiguration{ - NodeName: nodeName, - RemoteServers: remoteServers, - CertIPs: certIPs, - RootDir: rootDir, - JoinToken: joinToken, - YurtHubCertOrganizations: []string{"yurthub:tenant:foo"}, - Client: client, - }) - if err != nil { - t.Errorf("failed to new another yurt cert manager, %v", err) - return - } - newMgr.Start() - if !newMgr.Ready() { - t.Errorf("certificates can not be reused") - } - newMgr.Stop() - - os.RemoveAll(rootDir) + os.RemoveAll(workDir) } diff --git a/pkg/yurthub/kubernetes/rest/config_test.go b/pkg/yurthub/kubernetes/rest/config_test.go index ee5c450dfe0..bf76a17da04 100644 --- a/pkg/yurthub/kubernetes/rest/config_test.go +++ b/pkg/yurthub/kubernetes/rest/config_test.go @@ -17,7 +17,6 @@ limitations under the License. package rest import ( - "net" "net/url" "os" "testing" @@ -25,8 +24,9 @@ import ( "k8s.io/apimachinery/pkg/util/wait" - "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token" - "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token/testdata" + "github.com/openyurtio/openyurt/cmd/yurthub/app/options" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/manager" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/testdata" "github.com/openyurtio/openyurt/pkg/yurthub/healthchecker" ) @@ -39,22 +39,20 @@ func TestGetRestConfig(t *testing.T) { servers := map[string]int{"https://10.10.10.113:6443": 2} u, _ := url.Parse("https://10.10.10.113:6443") remoteServers := []*url.URL{u} - certIPs := []net.IP{net.ParseIP("127.0.0.1")} fakeHealthyChecker := healthchecker.NewFakeChecker(false, servers) - client, err := testdata.CreateCertFakeClient("../../certificate/token/testdata") + client, err := testdata.CreateCertFakeClient("../../certificate/testdata") if err != nil { t.Errorf("failed to create cert fake client, %v", err) return } - certManager, err := token.NewYurtHubCertManager(&token.CertificateManagerConfiguration{ + certManager, err := manager.NewYurtHubCertManager(&options.YurtHubOptions{ NodeName: nodeName, - RemoteServers: remoteServers, - CertIPs: certIPs, RootDir: testDir, + YurtHubHost: "127.0.0.1", JoinToken: "123456.abcdef1234567890", - Client: client, - }) + ClientForTest: client, + }, remoteServers) if err != nil { t.Errorf("failed to create certManager, %v", err) return diff --git a/pkg/yurthub/server/certificate_test.go b/pkg/yurthub/server/certificate_test.go index fe2a7a30445..2451f1b057a 100644 --- a/pkg/yurthub/server/certificate_test.go +++ b/pkg/yurthub/server/certificate_test.go @@ -19,7 +19,6 @@ package server import ( "bytes" "encoding/json" - "net" "net/http" "net/http/httptest" "net/url" @@ -29,8 +28,9 @@ import ( "k8s.io/apimachinery/pkg/util/wait" - "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token" - "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token/testdata" + "github.com/openyurtio/openyurt/cmd/yurthub/app/options" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/manager" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate/testdata" ) var ( @@ -40,20 +40,17 @@ var ( func TestUpdateTokenHandler(t *testing.T) { u, _ := url.Parse("https://10.10.10.113:6443") remoteServers := []*url.URL{u} - certIPs := []net.IP{net.ParseIP("127.0.0.1")} - client, err := testdata.CreateCertFakeClient("../certificate/token/testdata") + client, err := testdata.CreateCertFakeClient("../certificate/testdata") if err != nil { t.Errorf("failed to create cert fake client, %v", err) return } - certManager, err := token.NewYurtHubCertManager(&token.CertificateManagerConfiguration{ + certManager, err := manager.NewYurtHubCertManager(&options.YurtHubOptions{ NodeName: "foo", - RemoteServers: remoteServers, - CertIPs: certIPs, RootDir: testDir, JoinToken: "123456.abcdef1234567890", - Client: client, - }) + ClientForTest: client, + }, remoteServers) if err != nil { t.Errorf("failed to create certManager, %v", err) return @@ -63,7 +60,7 @@ func TestUpdateTokenHandler(t *testing.T) { defer os.RemoveAll(testDir) err = wait.PollImmediate(2*time.Second, 1*time.Minute, func() (done bool, err error) { - if certManager.Ready() { + if certManager.GetAPIServerClientCert() != nil { return true, nil } return false, nil From 237fe8fc0aef17265dfe47738e1721a3f96b7dc2 Mon Sep 17 00:00:00 2001 From: vie-serendipity <60083692+vie-serendipity@users.noreply.github.com> Date: Fri, 28 Jul 2023 10:15:19 +0800 Subject: [PATCH 61/93] check whether yurt manager's container is ready in e2e (#1631) * modify * modify * modify * modify * move check to deployYurtManager * modify * modify * modify --- test/e2e/cmd/init/converter.go | 13 +++++++++---- test/e2e/cmd/init/init.go | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/test/e2e/cmd/init/converter.go b/test/e2e/cmd/init/converter.go index 5f61b907afb..9108248b90e 100644 --- a/test/e2e/cmd/init/converter.go +++ b/test/e2e/cmd/init/converter.go @@ -286,13 +286,18 @@ func (c *ClusterConverter) deployYurtManager() error { if podList.Items[0].Status.Phase == corev1.PodRunning { for i := range podList.Items[0].Status.Conditions { if podList.Items[0].Status.Conditions[i].Type == corev1.PodReady && - podList.Items[0].Status.Conditions[i].Status == corev1.ConditionTrue { - return true, nil + podList.Items[0].Status.Conditions[i].Status != corev1.ConditionTrue { + klog.Infof("pod(%s/%s): %#v", podList.Items[0].Namespace, podList.Items[0].Name, podList.Items[0]) + return false, nil + } + if podList.Items[0].Status.Conditions[i].Type == corev1.ContainersReady && + podList.Items[0].Status.Conditions[i].Status != corev1.ConditionTrue { + klog.Info("yurt manager's container is not ready") + return false, nil } } } - klog.Infof("pod(%s/%s): %#v", podList.Items[0].Namespace, podList.Items[0].Name, podList.Items[0]) - return false, nil + return true, nil }) } diff --git a/test/e2e/cmd/init/init.go b/test/e2e/cmd/init/init.go index 13cb1981fd8..8e4f7753835 100644 --- a/test/e2e/cmd/init/init.go +++ b/test/e2e/cmd/init/init.go @@ -374,6 +374,7 @@ func (ki *Initializer) prepareKindConfigFile(kindConfigPath string) error { } func (ki *Initializer) configureAddons() error { + if err := ki.configureCoreDnsAddon(); err != nil { return err } From 7cf623d38e91768acfc5192f1513de379c2149a6 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <413621396@qq.com> Date: Wed, 9 Aug 2023 14:55:22 +0800 Subject: [PATCH 62/93] delete configmap when yurtstaticset is deleting (#1640) --- .../yurtstaticset/yurtstaticset_controller.go | 25 ++++++- .../yurtstaticset_controller_test.go | 65 ++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/pkg/controller/yurtstaticset/yurtstaticset_controller.go b/pkg/controller/yurtstaticset/yurtstaticset_controller.go index 0d59909e3f8..c3469a05645 100644 --- a/pkg/controller/yurtstaticset/yurtstaticset_controller.go +++ b/pkg/controller/yurtstaticset/yurtstaticset_controller.go @@ -283,12 +283,18 @@ func (r *ReconcileYurtStaticSet) Reconcile(_ context.Context, request reconcile. // Fetch the YurtStaticSet instance instance := &appsv1alpha1.YurtStaticSet{} if err := r.Get(context.TODO(), request.NamespacedName, instance); err != nil { + // if the yurtStaticSet does not exist, delete the specified configmap if exist. + if kerr.IsNotFound(err) { + return reconcile.Result{}, r.deleteConfigMap(request.Name, request.Namespace) + } klog.Errorf("Fail to get YurtStaticSet %v, %v", request.NamespacedName, err) return ctrl.Result{}, client.IgnoreNotFound(err) } if instance.DeletionTimestamp != nil { - return reconcile.Result{}, nil + // handle the deletion event + // delete the configMap which is created by yurtStaticSet + return reconcile.Result{}, r.deleteConfigMap(request.Name, request.Namespace) } var ( @@ -541,3 +547,20 @@ func (r *ReconcileYurtStaticSet) updateYurtStaticSetStatus(instance *appsv1alpha return reconcile.Result{}, nil } + +// deleteConfigMap delete the configMap if YurtStaticSet is deleting +func (r *ReconcileYurtStaticSet) deleteConfigMap(name, namespace string) error { + cmName := util.WithConfigMapPrefix(name) + configMap := &corev1.ConfigMap{} + if err := r.Get(context.TODO(), types.NamespacedName{Name: cmName, Namespace: namespace}, configMap); err != nil { + if kerr.IsNotFound(err) { + return nil + } + return err + } + if err := r.Delete(context.TODO(), configMap, &client.DeleteOptions{}); err != nil { + return err + } + klog.Infof(Format("Delete ConfigMap %s from YurtStaticSet %s", configMap.Name, name)) + return nil +} diff --git a/pkg/controller/yurtstaticset/yurtstaticset_controller_test.go b/pkg/controller/yurtstaticset/yurtstaticset_controller_test.go index f46556561d2..76eece0d502 100644 --- a/pkg/controller/yurtstaticset/yurtstaticset_controller_test.go +++ b/pkg/controller/yurtstaticset/yurtstaticset_controller_test.go @@ -27,7 +27,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" - fakeclint "sigs.k8s.io/controller-runtime/pkg/client/fake" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -113,7 +113,7 @@ func TestReconcile(t *testing.T) { for _, s := range strategy { instance.Spec.UpgradeStrategy = s - c := fakeclint.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).WithObjects(staticPods...).WithObjects(nodes...).Build() + c := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).WithObjects(staticPods...).WithObjects(nodes...).Build() var req = reconcile.Request{NamespacedName: types.NamespacedName{Namespace: metav1.NamespaceDefault, Name: TestStaticPodName}} rsp := ReconcileYurtStaticSet{ @@ -157,3 +157,64 @@ func Test_nodeTurnReady(t *testing.T) { } }) } + +func TestReconcileYurtStaticSetDeleteConfigMap(t *testing.T) { + staticPods := prepareStaticPods() + instance := &appsv1alpha1.YurtStaticSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: TestStaticPodName, + Namespace: metav1.NamespaceDefault, + }, + Spec: appsv1alpha1.YurtStaticSetSpec{ + StaticPodManifest: "nginx", + Template: corev1.PodTemplateSpec{}, + }, + } + cmList := []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "yurt-static-set-nginx", + Namespace: metav1.NamespaceDefault, + }, + }, + } + + scheme := runtime.NewScheme() + if err := appsv1alpha1.AddToScheme(scheme); err != nil { + t.Fatal("Fail to add yurt custom resource") + } + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatal("Fail to add kubernetes clint-go custom resource") + } + c := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).WithObjects(staticPods...).WithObjects(cmList...).Build() + + tests := []struct { + name string + yssName string + namespace string + wantErr bool + }{ + { + name: "test1", + yssName: TestStaticPodName, + namespace: metav1.NamespaceDefault, + wantErr: false, + }, + { + name: "test2", + yssName: TestStaticPodName, + namespace: metav1.NamespaceDefault, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &ReconcileYurtStaticSet{ + Client: c, + } + if err := r.deleteConfigMap(tt.yssName, tt.namespace); (err != nil) != tt.wantErr { + t.Errorf("deleteConfigMap() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From d89a805b2910431564bd6584d3c413ec8219946f Mon Sep 17 00:00:00 2001 From: Abyss <45425302+wangxye@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:05:03 +0800 Subject: [PATCH 63/93] feat: move yurt-device-controller into yurt-manager (#1607) * add a new crd iot.device/deviceservice/deviceprofile and incorporate it Signed-off-by: wangxye <1031989637@qq.com> --- .github/workflows/trivy-scan.yml | 2 +- Makefile | 2 + .../crds/iot.openyurt.io_deviceprofiles.yaml | 172 ++++++ .../crds/iot.openyurt.io_devices.yaml | 195 +++++++ .../crds/iot.openyurt.io_deviceservices.yaml | 141 +++++ .../yurt-manager-auto-generated.yaml | 78 +++ .../templates/yurt-manager-iot.yaml | 88 +++ cmd/yurt-iot-dock/app/core.go | 199 +++++++ cmd/yurt-iot-dock/app/options/options.go | 82 +++ cmd/yurt-iot-dock/yurt-iot-dock.go | 43 ++ go.mod | 11 +- go.sum | 31 ++ .../build/Dockerfile.yurt-iot-dock | 8 + .../release/Dockerfile.yurt-iot-dock | 14 + hack/make-rules/build.sh | 1 + hack/make-rules/image_build.sh | 1 + hack/make-rules/kustomize_to_chart.sh | 4 + hack/make-rules/local-up-openyurt.sh | 1 + pkg/apis/iot/v1alpha1/condition_const.go | 26 + pkg/apis/iot/v1alpha1/device_types.go | 175 ++++++ pkg/apis/iot/v1alpha1/deviceprofile_types.go | 122 +++++ pkg/apis/iot/v1alpha1/deviceservice_types.go | 121 +++++ .../iot/v1alpha1/zz_generated.deepcopy.go | 506 ++++++++++++++++++ .../platformadmin/platformadmin_controller.go | 6 + pkg/controller/platformadmin/util.go | 126 +++++ .../utils/{util.go => conditions.go} | 0 pkg/controller/platformadmin/utils/version.go | 31 ++ .../clients/edgex-foundry/device_client.go | 371 +++++++++++++ .../edgex-foundry/device_client_test.go | 202 +++++++ .../edgex-foundry/deviceprofile_client.go | 141 +++++ .../deviceprofile_client_test.go | 107 ++++ .../edgex-foundry/deviceservice_client.go | 166 ++++++ .../deviceservice_client_test.go | 126 +++++ .../clients/edgex-foundry/edgexobject.go | 21 + pkg/yurtiotdock/clients/edgex-foundry/util.go | 456 ++++++++++++++++ pkg/yurtiotdock/clients/errors.go | 29 + pkg/yurtiotdock/clients/interface.go | 93 ++++ .../controllers/device_controller.go | 295 ++++++++++ pkg/yurtiotdock/controllers/device_syncer.go | 237 ++++++++ .../controllers/deviceprofile_controller.go | 165 ++++++ .../controllers/deviceprofile_syncer.go | 214 ++++++++ .../controllers/deviceservice_controller.go | 220 ++++++++ .../controllers/deviceservice_syncer.go | 237 ++++++++ pkg/yurtiotdock/controllers/predicate.go | 48 ++ .../controllers/util/conditions.go | 120 +++++ .../controllers/util/fieldindexer.go | 62 +++ pkg/yurtiotdock/controllers/util/string.go | 30 ++ .../controllers/util/string_test.go | 70 +++ pkg/yurtiotdock/controllers/util/tools.go | 99 ++++ .../controllers/util/tools_test.go | 55 ++ .../controllers/well_known_labels.go | 21 + 51 files changed, 5769 insertions(+), 2 deletions(-) create mode 100644 charts/yurt-manager/crds/iot.openyurt.io_deviceprofiles.yaml create mode 100644 charts/yurt-manager/crds/iot.openyurt.io_devices.yaml create mode 100644 charts/yurt-manager/crds/iot.openyurt.io_deviceservices.yaml create mode 100644 charts/yurt-manager/templates/yurt-manager-iot.yaml create mode 100644 cmd/yurt-iot-dock/app/core.go create mode 100644 cmd/yurt-iot-dock/app/options/options.go create mode 100644 cmd/yurt-iot-dock/yurt-iot-dock.go create mode 100644 hack/dockerfiles/build/Dockerfile.yurt-iot-dock create mode 100644 hack/dockerfiles/release/Dockerfile.yurt-iot-dock create mode 100644 pkg/apis/iot/v1alpha1/device_types.go create mode 100644 pkg/apis/iot/v1alpha1/deviceprofile_types.go create mode 100644 pkg/apis/iot/v1alpha1/deviceservice_types.go create mode 100644 pkg/controller/platformadmin/util.go rename pkg/controller/platformadmin/utils/{util.go => conditions.go} (100%) create mode 100644 pkg/controller/platformadmin/utils/version.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/device_client.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/util.go create mode 100644 pkg/yurtiotdock/clients/errors.go create mode 100644 pkg/yurtiotdock/clients/interface.go create mode 100644 pkg/yurtiotdock/controllers/device_controller.go create mode 100644 pkg/yurtiotdock/controllers/device_syncer.go create mode 100644 pkg/yurtiotdock/controllers/deviceprofile_controller.go create mode 100644 pkg/yurtiotdock/controllers/deviceprofile_syncer.go create mode 100644 pkg/yurtiotdock/controllers/deviceservice_controller.go create mode 100644 pkg/yurtiotdock/controllers/deviceservice_syncer.go create mode 100644 pkg/yurtiotdock/controllers/predicate.go create mode 100644 pkg/yurtiotdock/controllers/util/conditions.go create mode 100644 pkg/yurtiotdock/controllers/util/fieldindexer.go create mode 100644 pkg/yurtiotdock/controllers/util/string.go create mode 100644 pkg/yurtiotdock/controllers/util/string_test.go create mode 100644 pkg/yurtiotdock/controllers/util/tools.go create mode 100644 pkg/yurtiotdock/controllers/util/tools_test.go create mode 100644 pkg/yurtiotdock/controllers/well_known_labels.go diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index eb2b54867c6..31dc32bb5d2 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [ yurthub, node-servant, yurt-manager ] + target: [ yurthub, node-servant, yurt-manager, yurt-iot-dock ] steps: - uses: actions/checkout@v3 with: diff --git a/Makefile b/Makefile index 49fe17030f4..fefb7fe0ca0 100644 --- a/Makefile +++ b/Makefile @@ -171,6 +171,8 @@ docker-push-yurt-tunnel-server: docker-buildx-builder docker-push-yurt-tunnel-agent: docker-buildx-builder docker buildx build --no-cache --push ${DOCKER_BUILD_ARGS} --platform ${TARGET_PLATFORMS} -f hack/dockerfiles/release/Dockerfile.yurt-tunnel-agent . -t ${IMAGE_REPO}/yurt-tunnel-agent:${GIT_VERSION} +docker-push-yurt-iot-dock: docker-buildx-builder + docker buildx build --no-cache --push ${DOCKER_BUILD_ARGS} --platform ${TARGET_PLATFORMS} -f hack/dockerfiles/release/Dockerfile.yurt-iot-dock . -t ${IMAGE_REPO}/yurt-iot-dock:${GIT_VERSION} generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. # hack/make-rule/generate_openapi.sh // TODO by kadisi diff --git a/charts/yurt-manager/crds/iot.openyurt.io_deviceprofiles.yaml b/charts/yurt-manager/crds/iot.openyurt.io_deviceprofiles.yaml new file mode 100644 index 00000000000..c1d92ed7a8e --- /dev/null +++ b/charts/yurt-manager/crds/iot.openyurt.io_deviceprofiles.yaml @@ -0,0 +1,172 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: deviceprofiles.iot.openyurt.io +spec: + group: iot.openyurt.io + names: + kind: DeviceProfile + listKind: DeviceProfileList + plural: deviceprofiles + shortNames: + - dp + singular: deviceprofile + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The nodepool of deviceProfile + jsonPath: .spec.nodePool + name: NODEPOOL + type: string + - description: The synced status of deviceProfile + jsonPath: .status.synced + name: SYNCED + type: boolean + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: DeviceProfile represents the attributes and operational capabilities + of a device. It is a template for which there can be multiple matching devices + within a given system. NOTE This struct is derived from edgex/go-mod-core-contracts/models/deviceprofile.go + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DeviceProfileSpec defines the desired state of DeviceProfile + properties: + description: + type: string + deviceCommands: + items: + properties: + isHidden: + type: boolean + name: + type: string + readWrite: + type: string + resourceOperations: + items: + properties: + defaultValue: + type: string + deviceResource: + type: string + mappings: + additionalProperties: + type: string + type: object + required: + - defaultValue + type: object + type: array + required: + - isHidden + - name + - readWrite + - resourceOperations + type: object + type: array + deviceResources: + items: + properties: + attributes: + additionalProperties: + type: string + type: object + description: + type: string + isHidden: + type: boolean + name: + type: string + properties: + properties: + assertion: + type: string + base: + type: string + defaultValue: + type: string + mask: + type: string + maximum: + type: string + mediaType: + type: string + minimum: + type: string + offset: + type: string + readWrite: + type: string + scale: + type: string + shift: + type: string + units: + type: string + valueType: + type: string + type: object + tag: + type: string + required: + - description + - isHidden + - name + - properties + type: object + type: array + labels: + description: Labels used to search for groups of profiles on EdgeX + Foundry + items: + type: string + type: array + manufacturer: + description: Manufacturer of the device + type: string + model: + description: Model of the device + type: string + nodePool: + description: NodePool specifies which nodePool the deviceProfile belongs + to + type: string + type: object + status: + description: DeviceProfileStatus defines the observed state of DeviceProfile + properties: + id: + type: string + synced: + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/yurt-manager/crds/iot.openyurt.io_devices.yaml b/charts/yurt-manager/crds/iot.openyurt.io_devices.yaml new file mode 100644 index 00000000000..a284a0f5b8f --- /dev/null +++ b/charts/yurt-manager/crds/iot.openyurt.io_devices.yaml @@ -0,0 +1,195 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: devices.iot.openyurt.io +spec: + group: iot.openyurt.io + names: + kind: Device + listKind: DeviceList + plural: devices + shortNames: + - dev + singular: device + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The nodepool of device + jsonPath: .spec.nodePool + name: NODEPOOL + type: string + - description: The synced status of device + jsonPath: .status.synced + name: SYNCED + type: boolean + - description: The managed status of device + jsonPath: .spec.managed + name: MANAGED + priority: 1 + type: boolean + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Device is the Schema for the devices API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DeviceSpec defines the desired state of Device + properties: + adminState: + description: Admin state (locked/unlocked) + type: string + description: + description: Information describing the device + type: string + deviceProperties: + additionalProperties: + properties: + desiredValue: + type: string + name: + type: string + putURL: + type: string + required: + - desiredValue + - name + type: object + description: TODO support the following field A list of auto-generated + events coming from the device AutoEvents []AutoEvent `json:"autoEvents"` + DeviceProperties represents the expected state of the device's properties + type: object + labels: + description: Other labels applied to the device to help with searching + items: + type: string + type: array + location: + description: 'Device service specific location (interface{} is an + empty interface so it can be anything) TODO: location type in edgex + is interface{}' + type: string + managed: + description: True means device is managed by cloud, cloud can update + the related fields False means cloud can't update the fields + type: boolean + nodePool: + description: NodePool indicates which nodePool the device comes from + type: string + notify: + type: boolean + operatingState: + description: Operating state (enabled/disabled) + type: string + profileName: + description: Associated Device Profile - Describes the device + type: string + protocols: + additionalProperties: + additionalProperties: + type: string + type: object + description: A map of supported protocols for the given device + type: object + serviceName: + description: Associated Device Service - One per device + type: string + required: + - notify + - profileName + - serviceName + type: object + status: + description: DeviceStatus defines the observed state of Device + properties: + adminState: + description: Admin state (locked/unlocked) + type: string + conditions: + description: current device state + items: + description: DeviceCondition describes current state of a Device. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of in place set condition. + type: string + type: object + type: array + deviceProperties: + additionalProperties: + properties: + actualValue: + type: string + getURL: + type: string + name: + type: string + required: + - actualValue + - name + type: object + description: it represents the actual state of the device's properties + type: object + edgeId: + type: string + lastConnected: + description: Time (milliseconds) that the device last provided any + feedback or responded to any request + format: int64 + type: integer + lastReported: + description: Time (milliseconds) that the device reported data to + the core microservice + format: int64 + type: integer + operatingState: + description: Operating state (up/down/unknown) + type: string + synced: + description: Synced indicates whether the device already exists on + both OpenYurt and edge platform + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/yurt-manager/crds/iot.openyurt.io_deviceservices.yaml b/charts/yurt-manager/crds/iot.openyurt.io_deviceservices.yaml new file mode 100644 index 00000000000..d4722a55546 --- /dev/null +++ b/charts/yurt-manager/crds/iot.openyurt.io_deviceservices.yaml @@ -0,0 +1,141 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: deviceservices.iot.openyurt.io +spec: + group: iot.openyurt.io + names: + kind: DeviceService + listKind: DeviceServiceList + plural: deviceservices + shortNames: + - dsvc + singular: deviceservice + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The nodepool of deviceService + jsonPath: .spec.nodePool + name: NODEPOOL + type: string + - description: The synced status of deviceService + jsonPath: .status.synced + name: SYNCED + type: boolean + - description: The managed status of deviceService + jsonPath: .spec.managed + name: MANAGED + priority: 1 + type: boolean + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: DeviceService is the Schema for the deviceservices API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DeviceServiceSpec defines the desired state of DeviceService + properties: + adminState: + description: Device Service Admin State + type: string + baseAddress: + type: string + description: + description: Information describing the device + type: string + labels: + description: tags or other labels applied to the device service for + search or other identification needs on the EdgeX Foundry + items: + type: string + type: array + managed: + description: True means deviceService is managed by cloud, cloud can + update the related fields False means cloud can't update the fields + type: boolean + nodePool: + description: NodePool indicates which nodePool the deviceService comes + from + type: string + required: + - baseAddress + type: object + status: + description: DeviceServiceStatus defines the observed state of DeviceService + properties: + adminState: + description: Device Service Admin State + type: string + conditions: + description: current deviceService state + items: + description: DeviceServiceCondition describes current state of a + Device. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of in place set condition. + type: string + type: object + type: array + edgeId: + description: the Id assigned by the edge platform + type: string + lastConnected: + description: time in milliseconds that the device last reported data + to the core + format: int64 + type: integer + lastReported: + description: time in milliseconds that the device last reported data + to the core + format: int64 + type: integer + synced: + description: Synced indicates whether the device already exists on + both OpenYurt and edge platform + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index 3dda7a3d1b4..72e35edad21 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -382,6 +382,84 @@ rules: - list - patch - watch +- apiGroups: + - iot.openyurt.io + resources: + - deviceprofiles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - iot.openyurt.io + resources: + - deviceprofiles/finalizers + verbs: + - update +- apiGroups: + - iot.openyurt.io + resources: + - deviceprofiles/status + verbs: + - get + - patch + - update +- apiGroups: + - iot.openyurt.io + resources: + - devices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - iot.openyurt.io + resources: + - devices/finalizers + verbs: + - update +- apiGroups: + - iot.openyurt.io + resources: + - devices/status + verbs: + - get + - patch + - update +- apiGroups: + - iot.openyurt.io + resources: + - deviceservices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - iot.openyurt.io + resources: + - deviceservices/finalizers + verbs: + - update +- apiGroups: + - iot.openyurt.io + resources: + - deviceservices/status + verbs: + - get + - patch + - update - apiGroups: - iot.openyurt.io resources: diff --git a/charts/yurt-manager/templates/yurt-manager-iot.yaml b/charts/yurt-manager/templates/yurt-manager-iot.yaml new file mode 100644 index 00000000000..ec415db5a72 --- /dev/null +++ b/charts/yurt-manager/templates/yurt-manager-iot.yaml @@ -0,0 +1,88 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: yurt-iot-dock +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - get + - apiGroups: + - iot.openyurt.io + resources: + - devices + - deviceservices + - deviceprofiles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - iot.openyurt.io + resources: + - devices/status + - deviceprofiles/status + - deviceservices/status + verbs: + - get + - patch + - update + - apiGroups: + - iot.openyurt.io + resources: + - devices/finalizers + - deviceprofiles/finalizers + - deviceservices/finalizers + verbs: + - update + - apiGroups: + - apps.openyurt.io + resources: + - nodepools + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: yurt-iot-dock +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: yurt-iot-dock +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:authenticated \ No newline at end of file diff --git a/cmd/yurt-iot-dock/app/core.go b/cmd/yurt-iot-dock/app/core.go new file mode 100644 index 00000000000..39e30bcaf30 --- /dev/null +++ b/cmd/yurt-iot-dock/app/core.go @@ -0,0 +1,199 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/klog/v2" + "k8s.io/klog/v2/klogr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + _ = clientgoscheme.AddToScheme(scheme) + + _ = apis.AddToScheme(clientgoscheme.Scheme) + _ = apis.AddToScheme(scheme) + + // +kubebuilder:scaffold:scheme +} + +func NewCmdYurtIoTDock(stopCh <-chan struct{}) *cobra.Command { + yurtIoTDockOptions := options.NewYurtIoTDockOptions() + cmd := &cobra.Command{ + Use: "yurt-iot-dock", + Short: "Launch yurt-iot-dock", + Long: "Launch yurt-iot-dock", + Run: func(cmd *cobra.Command, args []string) { + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + klog.V(1).Infof("FLAG: --%s=%q", flag.Name, flag.Value) + }) + if err := options.ValidateOptions(yurtIoTDockOptions); err != nil { + klog.Fatalf("validate options: %v", err) + } + Run(yurtIoTDockOptions, stopCh) + }, + } + + yurtIoTDockOptions.AddFlags(cmd.Flags()) + return cmd +} + +func Run(opts *options.YurtIoTDockOptions, stopCh <-chan struct{}) { + ctrl.SetLogger(klogr.New()) + cfg := ctrl.GetConfigOrDie() + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: opts.MetricsAddr, + HealthProbeBindAddress: opts.ProbeAddr, + LeaderElection: opts.EnableLeaderElection, + LeaderElectionID: "yurt-iot-dock", + Namespace: opts.Namespace, + }) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + // perform preflight check + setupLog.Info("[preflight] Running pre-flight checks") + if err := preflightCheck(mgr, opts); err != nil { + setupLog.Error(err, "failed to run pre-flight checks") + os.Exit(1) + } + + // register the field indexers + setupLog.Info("[preflight] Registering the field indexers") + if err := util.RegisterFieldIndexers(mgr.GetFieldIndexer()); err != nil { + setupLog.Error(err, "failed to register field indexers") + os.Exit(1) + } + + // get nodepool where yurt-iot-dock run + if opts.Nodepool == "" { + opts.Nodepool, err = util.GetNodePool(mgr.GetConfig()) + if err != nil { + setupLog.Error(err, "failed to get the nodepool where yurt-iot-dock run") + os.Exit(1) + } + } + + // setup the DeviceProfile Reconciler and Syncer + if err = (&controllers.DeviceProfileReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, opts); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DeviceProfile") + os.Exit(1) + } + dfs, err := controllers.NewDeviceProfileSyncer(mgr.GetClient(), opts) + if err != nil { + setupLog.Error(err, "unable to create syncer", "syncer", "DeviceProfile") + os.Exit(1) + } + err = mgr.Add(dfs.NewDeviceProfileSyncerRunnable()) + if err != nil { + setupLog.Error(err, "unable to create syncer runnable", "syncer", "DeviceProfile") + os.Exit(1) + } + + // setup the Device Reconciler and Syncer + if err = (&controllers.DeviceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, opts); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Device") + os.Exit(1) + } + ds, err := controllers.NewDeviceSyncer(mgr.GetClient(), opts) + if err != nil { + setupLog.Error(err, "unable to create syncer", "controller", "Device") + os.Exit(1) + } + err = mgr.Add(ds.NewDeviceSyncerRunnable()) + if err != nil { + setupLog.Error(err, "unable to create syncer runnable", "syncer", "Device") + os.Exit(1) + } + + // setup the DeviceService Reconciler and Syncer + if err = (&controllers.DeviceServiceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr, opts); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DeviceService") + os.Exit(1) + } + dss, err := controllers.NewDeviceServiceSyncer(mgr.GetClient(), opts) + if err != nil { + setupLog.Error(err, "unable to create syncer", "syncer", "DeviceService") + os.Exit(1) + } + err = mgr.Add(dss.NewDeviceServiceSyncerRunnable()) + if err != nil { + setupLog.Error(err, "unable to create syncer runnable", "syncer", "DeviceService") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("check", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("[run controllers] Starting manager, acting on " + fmt.Sprintf("[NodePool: %s, Namespace: %s]", opts.Nodepool, opts.Namespace)) + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "failed to running manager") + os.Exit(1) + } +} + +func preflightCheck(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { + client, err := kubernetes.NewForConfig(mgr.GetConfig()) + if err != nil { + return err + } + if _, err := client.CoreV1().Namespaces().Get(context.TODO(), opts.Namespace, metav1.GetOptions{}); err != nil { + return err + } + return nil +} diff --git a/cmd/yurt-iot-dock/app/options/options.go b/cmd/yurt-iot-dock/app/options/options.go new file mode 100644 index 00000000000..08d9e37a920 --- /dev/null +++ b/cmd/yurt-iot-dock/app/options/options.go @@ -0,0 +1,82 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package options + +import ( + "fmt" + "net" + + "github.com/spf13/pflag" +) + +// YurtIoTDockOptions is the main settings for the yurt-iot-dock +type YurtIoTDockOptions struct { + MetricsAddr string + ProbeAddr string + EnableLeaderElection bool + Nodepool string + Namespace string + CoreDataAddr string + CoreMetadataAddr string + CoreCommandAddr string + EdgeSyncPeriod uint +} + +func NewYurtIoTDockOptions() *YurtIoTDockOptions { + return &YurtIoTDockOptions{ + MetricsAddr: ":8080", + ProbeAddr: ":8080", + EnableLeaderElection: false, + Nodepool: "", + Namespace: "default", + CoreDataAddr: "edgex-core-data:59880", + CoreMetadataAddr: "edgex-core-metadata:59881", + CoreCommandAddr: "edgex-core-command:59882", + EdgeSyncPeriod: 5, + } +} + +func ValidateOptions(options *YurtIoTDockOptions) error { + if err := ValidateEdgePlatformAddress(options); err != nil { + return err + } + return nil +} + +func (o *YurtIoTDockOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.MetricsAddr, "metrics-bind-address", o.MetricsAddr, "The address the metric endpoint binds to.") + fs.StringVar(&o.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + fs.BoolVar(&o.EnableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+"Enabling this will ensure there is only one active controller manager.") + fs.StringVar(&o.Nodepool, "nodepool", "", "The nodePool deviceController is deployed in.(just for debugging)") + fs.StringVar(&o.Namespace, "namespace", "default", "The cluster namespace for edge resources synchronization.") + fs.StringVar(&o.CoreDataAddr, "core-data-address", "edgex-core-data:59880", "The address of edge core-data service.") + fs.StringVar(&o.CoreMetadataAddr, "core-metadata-address", "edgex-core-metadata:59881", "The address of edge core-metadata service.") + fs.StringVar(&o.CoreCommandAddr, "core-command-address", "edgex-core-command:59882", "The address of edge core-command service.") + fs.UintVar(&o.EdgeSyncPeriod, "edge-sync-period", 5, "The period of the device management platform synchronizing the device status to the cloud.(in seconds,not less than 5 seconds)") +} + +func ValidateEdgePlatformAddress(options *YurtIoTDockOptions) error { + addrs := []string{options.CoreDataAddr, options.CoreMetadataAddr, options.CoreCommandAddr} + for _, addr := range addrs { + if addr != "" { + if _, _, err := net.SplitHostPort(addr); err != nil { + return fmt.Errorf("invalid address: %s", err) + } + } + } + return nil +} diff --git a/cmd/yurt-iot-dock/yurt-iot-dock.go b/cmd/yurt-iot-dock/yurt-iot-dock.go new file mode 100644 index 00000000000..c647e5241c0 --- /dev/null +++ b/cmd/yurt-iot-dock/yurt-iot-dock.go @@ -0,0 +1,43 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "flag" + "math/rand" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app" +) + +func main() { + rand.Seed(time.Now().UnixNano()) + klog.InitFlags(nil) + defer klog.Flush() + + cmd := app.NewCmdYurtIoTDock(wait.NeverStop) + cmd.Flags().AddGoFlagSet(flag.CommandLine) + if err := cmd.Execute(); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod index f79da3cf042..53dcd2e2ad0 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,13 @@ go 1.18 require ( github.com/aliyun/alibaba-cloud-sdk-go v1.62.156 github.com/davecgh/go-spew v1.1.1 + github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0 + github.com/go-resty/resty/v2 v2.4.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-version v1.6.0 + github.com/jarcoal/httpmock v1.3.0 github.com/onsi/ginkgo/v2 v2.1.4 github.com/onsi/gomega v1.19.0 github.com/opencontainers/selinux v1.11.0 @@ -24,6 +27,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 + golang.org/x/net v0.9.0 golang.org/x/sys v0.10.0 google.golang.org/grpc v1.56.2 gopkg.in/cheggaaa/pb.v1 v1.0.28 @@ -86,10 +90,14 @@ require ( github.com/felixge/httpsnoop v1.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fvbommel/sortorder v1.0.1 // indirect + github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-logr/logr v0.4.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -103,6 +111,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect @@ -124,6 +133,7 @@ require ( github.com/spf13/afero v1.6.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect + github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect @@ -139,7 +149,6 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.5.0 // indirect - golang.org/x/net v0.9.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/term v0.7.0 // indirect diff --git a/go.sum b/go.sum index 4c71d04330b..c77bd68f7d3 100644 --- a/go.sum +++ b/go.sum @@ -175,6 +175,8 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0 h1:8Svk1HTehXEgwxgyA4muVhSkP3D9n1q+oSHI3B1Ac90= +github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0/go.mod h1:4/e61acxVkhQWCTjQ4XcHVJDnrMDloFsZZB1B6STCRw= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -214,6 +216,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -245,6 +249,16 @@ github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfT github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k= +github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -385,6 +399,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ishidawataru/sctp v0.0.0-20190723014705-7c296d48a2b5/go.mod h1:DM4VvS+hD/kDi1U1QsX2fnZowwBhqD0Dk3bRPKF/Oc8= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -422,6 +438,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -429,6 +446,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubernetes/kubernetes v1.22.3 h1:0gYnqsr5nZiAO+iDkEU7RJ6Ne2CMyoinJXVm5qVSTiE= github.com/kubernetes/kubernetes v1.22.3/go.mod h1:Snea7fgIObGgHmLbUJ3OgjGEr5bjj16iEdp5oHS6eS8= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= @@ -452,6 +471,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= @@ -543,6 +563,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -591,6 +612,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= @@ -676,6 +699,8 @@ github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3C github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= @@ -756,6 +781,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -837,6 +863,7 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -914,8 +941,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -936,6 +965,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1086,6 +1116,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/hack/dockerfiles/build/Dockerfile.yurt-iot-dock b/hack/dockerfiles/build/Dockerfile.yurt-iot-dock new file mode 100644 index 00000000000..3e684bb6866 --- /dev/null +++ b/hack/dockerfiles/build/Dockerfile.yurt-iot-dock @@ -0,0 +1,8 @@ +# multi-arch image building for yurt-iot-dock + +FROM --platform=${TARGETPLATFORM} alpine:3.17 +ARG TARGETOS TARGETARCH MIRROR_REPO +RUN if [ ! -z "${MIRROR_REPO+x}" ]; then sed -i "s/dl-cdn.alpinelinux.org/${MIRROR_REPO}/g" /etc/apk/repositories; fi && \ + apk add ca-certificates bash libc6-compat iptables ip6tables && update-ca-certificates && rm /var/cache/apk/* +COPY ./_output/local/bin/${TARGETOS}/${TARGETARCH}/yurt-iot-dock /usr/local/bin/yurt-iot-dock +ENTRYPOINT ["/usr/local/bin/yurt-iot-dock"] \ No newline at end of file diff --git a/hack/dockerfiles/release/Dockerfile.yurt-iot-dock b/hack/dockerfiles/release/Dockerfile.yurt-iot-dock new file mode 100644 index 00000000000..d73a35cb790 --- /dev/null +++ b/hack/dockerfiles/release/Dockerfile.yurt-iot-dock @@ -0,0 +1,14 @@ +# multi-arch image building for yurt-iot-dock + +FROM --platform=${BUILDPLATFORM} golang:1.18 as builder +ADD . /build +ARG TARGETOS TARGETARCH GIT_VERSION GOPROXY MIRROR_REPO +WORKDIR /build/ +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GIT_VERSION=${GIT_VERSION} make build WHAT=cmd/yurt-iot-dock + +FROM --platform=${TARGETPLATFORM} alpine:3.17 +ARG TARGETOS TARGETARCH MIRROR_REPO +RUN if [ ! -z "${MIRROR_REPO+x}" ]; then sed -i "s/dl-cdn.alpinelinux.org/${MIRROR_REPO}/g" /etc/apk/repositories; fi && \ + apk add ca-certificates bash libc6-compat iptables ip6tables && update-ca-certificates && rm /var/cache/apk/* +COPY --from=builder /build/_output/local/bin/${TARGETOS}/${TARGETARCH}/yurt-iot-dock /usr/local/bin/yurt-iot-dock +ENTRYPOINT ["/usr/local/bin/yurt-iot-dock"] \ No newline at end of file diff --git a/hack/make-rules/build.sh b/hack/make-rules/build.sh index 1422dd5df7d..5e28026b658 100755 --- a/hack/make-rules/build.sh +++ b/hack/make-rules/build.sh @@ -25,6 +25,7 @@ readonly YURT_ALL_TARGETS=( yurt-node-servant yurthub yurt-manager + yurt-iot-dock ) # clean old binaries at GOOS and GOARCH diff --git a/hack/make-rules/image_build.sh b/hack/make-rules/image_build.sh index b069cfcbc0b..1f4afa71d07 100755 --- a/hack/make-rules/image_build.sh +++ b/hack/make-rules/image_build.sh @@ -25,6 +25,7 @@ readonly IMAGE_TARGETS=( yurt-node-servant yurthub yurt-manager + yurt-iot-dock ) http_proxy=${http_proxy:-} diff --git a/hack/make-rules/kustomize_to_chart.sh b/hack/make-rules/kustomize_to_chart.sh index a9442b31b2a..f8e0f98eff8 100755 --- a/hack/make-rules/kustomize_to_chart.sh +++ b/hack/make-rules/kustomize_to_chart.sh @@ -192,6 +192,10 @@ EOF mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_yurtappdaemons.apps.openyurt.io.yaml ${crd_dir}/apps.openyurt.io_yurtappdaemons.yaml mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_yurtappsets.apps.openyurt.io.yaml ${crd_dir}/apps.openyurt.io_yurtappsets.yaml mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_gateways.raven.openyurt.io.yaml ${crd_dir}/raven.openyurt.io_gateways.yaml + mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_platformadmins.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_platformadmins.yaml + mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_devices.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_devices.yaml + mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_deviceservices.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_deviceservices.yaml + mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_deviceprofiles.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_deviceprofiles.yaml # rbac dir local rbac_kustomization_resources="" diff --git a/hack/make-rules/local-up-openyurt.sh b/hack/make-rules/local-up-openyurt.sh index 5181eec46f8..88c709b0b4f 100755 --- a/hack/make-rules/local-up-openyurt.sh +++ b/hack/make-rules/local-up-openyurt.sh @@ -56,6 +56,7 @@ readonly REQUIRED_IMAGES=( openyurt/node-servant openyurt/yurt-manager openyurt/yurthub + openyurt/yurt-iot-dock ) readonly LOCAL_ARCH=$(go env GOHOSTARCH) diff --git a/pkg/apis/iot/v1alpha1/condition_const.go b/pkg/apis/iot/v1alpha1/condition_const.go index 7f1c8b5e84e..498c9c3e87e 100644 --- a/pkg/apis/iot/v1alpha1/condition_const.go +++ b/pkg/apis/iot/v1alpha1/condition_const.go @@ -35,4 +35,30 @@ const ( DeploymentProvisioningReason = "DeploymentProvisioning" DeploymentProvisioningFailedReason = "DeploymentProvisioningFailed" + + // DeviceSyncedCondition indicates that the device exists in both OpenYurt and edge platform + DeviceSyncedCondition DeviceConditionType = "DeviceSynced" + + DeviceManagingReason = "This device is not managed by openyurt" + + DeviceCreateSyncedReason = "Failed to create device on edge platform" + + // DeviceManagingCondition indicates that the device is being managed by cloud and its properties are being reconciled + DeviceManagingCondition DeviceConditionType = "DeviceManaging" + + DeviceVistedCoreMetadataSyncedReason = "Failed to visit the EdgeX core-metadata-service" + + DeviceUpdateStateReason = "Failed to update AdminState or OperatingState of device on edge platform" + + // DeviceServiceSyncedCondition indicates that the deviceService exists in both OpenYurt and edge platform + DeviceServiceSyncedCondition DeviceServiceConditionType = "DeviceServiceSynced" + + DeviceServiceManagingReason = "This deviceService is not managed by openyurt" + + // DeviceServiceManagingCondition indicates that the deviceService is being managed by cloud and its field are being reconciled + DeviceServiceManagingCondition DeviceServiceConditionType = "DeviceServiceManaging" + + DeviceServiceCreateSyncedReason = "Failed to add DeviceService to EdgeX" + + DeviceServiceUpdateStatusSyncedReason = "Failed to update DeviceService status" ) diff --git a/pkg/apis/iot/v1alpha1/device_types.go b/pkg/apis/iot/v1alpha1/device_types.go new file mode 100644 index 00000000000..9a1bafd257f --- /dev/null +++ b/pkg/apis/iot/v1alpha1/device_types.go @@ -0,0 +1,175 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DeviceFinalizer = "iot.openyurt.io/device" +) + +// DeviceConditionType indicates valid conditions type of a Device. +type DeviceConditionType string + +type AdminState string + +const ( + Locked AdminState = "LOCKED" + UnLocked AdminState = "UNLOCKED" +) + +type OperatingState string + +const ( + Unknown OperatingState = "UNKNOWN" + Up OperatingState = "UP" + Down OperatingState = "DOWN" +) + +type ProtocolProperties map[string]string + +// DeviceSpec defines the desired state of Device +type DeviceSpec struct { + // Information describing the device + Description string `json:"description,omitempty"` + // Admin state (locked/unlocked) + AdminState AdminState `json:"adminState,omitempty"` + // Operating state (enabled/disabled) + OperatingState OperatingState `json:"operatingState,omitempty"` + // A map of supported protocols for the given device + Protocols map[string]ProtocolProperties `json:"protocols,omitempty"` + // Other labels applied to the device to help with searching + Labels []string `json:"labels,omitempty"` + // Device service specific location (interface{} is an empty interface so + // it can be anything) + // TODO: location type in edgex is interface{} + Location string `json:"location,omitempty"` + // Associated Device Service - One per device + Service string `json:"serviceName"` + // Associated Device Profile - Describes the device + Profile string `json:"profileName"` + Notify bool `json:"notify"` + // True means device is managed by cloud, cloud can update the related fields + // False means cloud can't update the fields + Managed bool `json:"managed,omitempty"` + // NodePool indicates which nodePool the device comes from + NodePool string `json:"nodePool,omitempty"` + // TODO support the following field + // A list of auto-generated events coming from the device + // AutoEvents []AutoEvent `json:"autoEvents"` + // DeviceProperties represents the expected state of the device's properties + DeviceProperties map[string]DesiredPropertyState `json:"deviceProperties,omitempty"` +} + +type DesiredPropertyState struct { + Name string `json:"name"` + PutURL string `json:"putURL,omitempty"` + DesiredValue string `json:"desiredValue"` +} + +type ActualPropertyState struct { + Name string `json:"name"` + GetURL string `json:"getURL,omitempty"` + ActualValue string `json:"actualValue"` +} + +// DeviceStatus defines the observed state of Device +type DeviceStatus struct { + // Time (milliseconds) that the device last provided any feedback or + // responded to any request + LastConnected int64 `json:"lastConnected,omitempty"` + // Time (milliseconds) that the device reported data to the core + // microservice + LastReported int64 `json:"lastReported,omitempty"` + // Synced indicates whether the device already exists on both OpenYurt and edge platform + Synced bool `json:"synced,omitempty"` + // it represents the actual state of the device's properties + DeviceProperties map[string]ActualPropertyState `json:"deviceProperties,omitempty"` + EdgeId string `json:"edgeId,omitempty"` + // Admin state (locked/unlocked) + AdminState AdminState `json:"adminState,omitempty"` + // Operating state (up/down/unknown) + OperatingState OperatingState `json:"operatingState,omitempty"` + // current device state + // +optional + Conditions []DeviceCondition `json:"conditions,omitempty"` +} + +// DeviceCondition describes current state of a Device. +type DeviceCondition struct { + // Type of in place set condition. + Type DeviceConditionType `json:"type,omitempty"` + + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status,omitempty"` + + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=dev +// +kubebuilder:printcolumn:name="NODEPOOL",type="string",JSONPath=".spec.nodePool",description="The nodepool of device" +// +kubebuilder:printcolumn:name="SYNCED",type="boolean",JSONPath=".status.synced",description="The synced status of device" +// +kubebuilder:printcolumn:name="MANAGED",type="boolean",priority=1,JSONPath=".spec.managed",description="The managed status of device" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// Device is the Schema for the devices API +type Device struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeviceSpec `json:"spec,omitempty"` + Status DeviceStatus `json:"status,omitempty"` +} + +func (d *Device) SetConditions(conditions []DeviceCondition) { + d.Status.Conditions = conditions +} + +func (d *Device) GetConditions() []DeviceCondition { + return d.Status.Conditions +} + +func (d *Device) IsAddedToEdgeX() bool { + return d.Status.Synced +} + +//+kubebuilder:object:root=true + +// DeviceList contains a list of Device +type DeviceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Device `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Device{}, &DeviceList{}) +} diff --git a/pkg/apis/iot/v1alpha1/deviceprofile_types.go b/pkg/apis/iot/v1alpha1/deviceprofile_types.go new file mode 100644 index 00000000000..2d25605bea9 --- /dev/null +++ b/pkg/apis/iot/v1alpha1/deviceprofile_types.go @@ -0,0 +1,122 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DeviceProfileFinalizer = "iot.openyurt.io/deviceprofile" +) + +type DeviceResource struct { + Description string `json:"description"` + Name string `json:"name"` + Tag string `json:"tag,omitempty"` + IsHidden bool `json:"isHidden"` + Properties ResourceProperties `json:"properties"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +type ResourceProperties struct { + ReadWrite string `json:"readWrite,omitempty"` // Read/Write Permissions set for this property + Minimum string `json:"minimum,omitempty"` // Minimum value that can be get/set from this property + Maximum string `json:"maximum,omitempty"` // Maximum value that can be get/set from this property + DefaultValue string `json:"defaultValue,omitempty"` // Default value set to this property if no argument is passed + Mask string `json:"mask,omitempty"` // Mask to be applied prior to get/set of property + Shift string `json:"shift,omitempty"` // Shift to be applied after masking, prior to get/set of property + Scale string `json:"scale,omitempty"` // Multiplicative factor to be applied after shifting, prior to get/set of property + Offset string `json:"offset,omitempty"` // Additive factor to be applied after multiplying, prior to get/set of property + Base string `json:"base,omitempty"` // Base for property to be applied to, leave 0 for no power operation (i.e. base ^ property: 2 ^ 10) + Assertion string `json:"assertion,omitempty"` + MediaType string `json:"mediaType,omitempty"` + Units string `json:"units,omitempty"` + ValueType string `json:"valueType,omitempty"` +} + +type DeviceCommand struct { + Name string `json:"name"` + IsHidden bool `json:"isHidden"` + ReadWrite string `json:"readWrite"` + ResourceOperations []ResourceOperation `json:"resourceOperations"` +} + +type ResourceOperation struct { + DeviceResource string `json:"deviceResource,omitempty"` + Mappings map[string]string `json:"mappings,omitempty"` + DefaultValue string `json:"defaultValue"` +} + +// DeviceProfileSpec defines the desired state of DeviceProfile +type DeviceProfileSpec struct { + // NodePool specifies which nodePool the deviceProfile belongs to + NodePool string `json:"nodePool,omitempty"` + Description string `json:"description,omitempty"` + // Manufacturer of the device + Manufacturer string `json:"manufacturer,omitempty"` + // Model of the device + Model string `json:"model,omitempty"` + // Labels used to search for groups of profiles on EdgeX Foundry + Labels []string `json:"labels,omitempty"` + DeviceResources []DeviceResource `json:"deviceResources,omitempty"` + DeviceCommands []DeviceCommand `json:"deviceCommands,omitempty"` +} + +// DeviceProfileStatus defines the observed state of DeviceProfile +type DeviceProfileStatus struct { + EdgeId string `json:"id,omitempty"` + Synced bool `json:"synced,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=dp +// +kubebuilder:printcolumn:name="NODEPOOL",type="string",JSONPath=".spec.nodePool",description="The nodepool of deviceProfile" +// +kubebuilder:printcolumn:name="SYNCED",type="boolean",JSONPath=".status.synced",description="The synced status of deviceProfile" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// DeviceProfile represents the attributes and operational capabilities of a device. +// It is a template for which there can be multiple matching devices within a given system. +// NOTE This struct is derived from +// edgex/go-mod-core-contracts/models/deviceprofile.go +type DeviceProfile struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeviceProfileSpec `json:"spec,omitempty"` + Status DeviceProfileStatus `json:"status,omitempty"` +} + +func (dp *DeviceProfile) IsAddedToEdgeX() bool { + return dp.Status.Synced +} + +//+kubebuilder:object:root=true + +// DeviceProfileList contains a list of DeviceProfile +type DeviceProfileList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DeviceProfile `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DeviceProfile{}, &DeviceProfileList{}) +} diff --git a/pkg/apis/iot/v1alpha1/deviceservice_types.go b/pkg/apis/iot/v1alpha1/deviceservice_types.go new file mode 100644 index 00000000000..4720b4e46bf --- /dev/null +++ b/pkg/apis/iot/v1alpha1/deviceservice_types.go @@ -0,0 +1,121 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DeviceServiceFinalizer = "iot.openyurt.io/deviceservice" +) + +// DeviceServiceConditionType indicates valid conditions type of a Device Service. +type DeviceServiceConditionType string + +// DeviceServiceSpec defines the desired state of DeviceService +type DeviceServiceSpec struct { + BaseAddress string `json:"baseAddress"` + // Information describing the device + Description string `json:"description,omitempty"` + // tags or other labels applied to the device service for search or other + // identification needs on the EdgeX Foundry + Labels []string `json:"labels,omitempty"` + // Device Service Admin State + AdminState AdminState `json:"adminState,omitempty"` + // True means deviceService is managed by cloud, cloud can update the related fields + // False means cloud can't update the fields + Managed bool `json:"managed,omitempty"` + // NodePool indicates which nodePool the deviceService comes from + NodePool string `json:"nodePool,omitempty"` +} + +// DeviceServiceStatus defines the observed state of DeviceService +type DeviceServiceStatus struct { + // Synced indicates whether the device already exists on both OpenYurt and edge platform + Synced bool `json:"synced,omitempty"` + // the Id assigned by the edge platform + EdgeId string `json:"edgeId,omitempty"` + // time in milliseconds that the device last reported data to the core + LastConnected int64 `json:"lastConnected,omitempty"` + // time in milliseconds that the device last reported data to the core + LastReported int64 `json:"lastReported,omitempty"` + // Device Service Admin State + AdminState AdminState `json:"adminState,omitempty"` + // current deviceService state + // +optional + Conditions []DeviceServiceCondition `json:"conditions,omitempty"` +} + +// DeviceServiceCondition describes current state of a Device. +type DeviceServiceCondition struct { + // Type of in place set condition. + Type DeviceServiceConditionType `json:"type,omitempty"` + + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status,omitempty"` + + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=dsvc +// +kubebuilder:printcolumn:name="NODEPOOL",type="string",JSONPath=".spec.nodePool",description="The nodepool of deviceService" +// +kubebuilder:printcolumn:name="SYNCED",type="boolean",JSONPath=".status.synced",description="The synced status of deviceService" +// +kubebuilder:printcolumn:name="MANAGED",type="boolean",priority=1,JSONPath=".spec.managed",description="The managed status of deviceService" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// DeviceService is the Schema for the deviceservices API +type DeviceService struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeviceServiceSpec `json:"spec,omitempty"` + Status DeviceServiceStatus `json:"status,omitempty"` +} + +func (ds *DeviceService) SetConditions(conditions []DeviceServiceCondition) { + ds.Status.Conditions = conditions +} + +func (ds *DeviceService) GetConditions() []DeviceServiceCondition { + return ds.Status.Conditions +} + +//+kubebuilder:object:root=true + +// DeviceServiceList contains a list of DeviceService +type DeviceServiceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DeviceService `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DeviceService{}, &DeviceServiceList{}) +} diff --git a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go index 62aaa062a52..3162ed46c6e 100644 --- a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,21 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActualPropertyState) DeepCopyInto(out *ActualPropertyState) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActualPropertyState. +func (in *ActualPropertyState) DeepCopy() *ActualPropertyState { + if in == nil { + return nil + } + out := new(ActualPropertyState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentTemplateSpec) DeepCopyInto(out *DeploymentTemplateSpec) { *out = *in @@ -42,6 +57,439 @@ func (in *DeploymentTemplateSpec) DeepCopy() *DeploymentTemplateSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DesiredPropertyState) DeepCopyInto(out *DesiredPropertyState) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DesiredPropertyState. +func (in *DesiredPropertyState) DeepCopy() *DesiredPropertyState { + if in == nil { + return nil + } + out := new(DesiredPropertyState) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Device) DeepCopyInto(out *Device) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Device. +func (in *Device) DeepCopy() *Device { + if in == nil { + return nil + } + out := new(Device) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Device) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceCommand) DeepCopyInto(out *DeviceCommand) { + *out = *in + if in.ResourceOperations != nil { + in, out := &in.ResourceOperations, &out.ResourceOperations + *out = make([]ResourceOperation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceCommand. +func (in *DeviceCommand) DeepCopy() *DeviceCommand { + if in == nil { + return nil + } + out := new(DeviceCommand) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceCondition) DeepCopyInto(out *DeviceCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceCondition. +func (in *DeviceCondition) DeepCopy() *DeviceCondition { + if in == nil { + return nil + } + out := new(DeviceCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceList) DeepCopyInto(out *DeviceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Device, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceList. +func (in *DeviceList) DeepCopy() *DeviceList { + if in == nil { + return nil + } + out := new(DeviceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceProfile) DeepCopyInto(out *DeviceProfile) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceProfile. +func (in *DeviceProfile) DeepCopy() *DeviceProfile { + if in == nil { + return nil + } + out := new(DeviceProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceProfile) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceProfileList) DeepCopyInto(out *DeviceProfileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeviceProfile, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceProfileList. +func (in *DeviceProfileList) DeepCopy() *DeviceProfileList { + if in == nil { + return nil + } + out := new(DeviceProfileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceProfileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceProfileSpec) DeepCopyInto(out *DeviceProfileSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DeviceResources != nil { + in, out := &in.DeviceResources, &out.DeviceResources + *out = make([]DeviceResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DeviceCommands != nil { + in, out := &in.DeviceCommands, &out.DeviceCommands + *out = make([]DeviceCommand, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceProfileSpec. +func (in *DeviceProfileSpec) DeepCopy() *DeviceProfileSpec { + if in == nil { + return nil + } + out := new(DeviceProfileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceProfileStatus) DeepCopyInto(out *DeviceProfileStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceProfileStatus. +func (in *DeviceProfileStatus) DeepCopy() *DeviceProfileStatus { + if in == nil { + return nil + } + out := new(DeviceProfileStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceResource) DeepCopyInto(out *DeviceResource) { + *out = *in + out.Properties = in.Properties + if in.Attributes != nil { + in, out := &in.Attributes, &out.Attributes + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceResource. +func (in *DeviceResource) DeepCopy() *DeviceResource { + if in == nil { + return nil + } + out := new(DeviceResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceService) DeepCopyInto(out *DeviceService) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceService. +func (in *DeviceService) DeepCopy() *DeviceService { + if in == nil { + return nil + } + out := new(DeviceService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceService) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceServiceCondition) DeepCopyInto(out *DeviceServiceCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceServiceCondition. +func (in *DeviceServiceCondition) DeepCopy() *DeviceServiceCondition { + if in == nil { + return nil + } + out := new(DeviceServiceCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceServiceList) DeepCopyInto(out *DeviceServiceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeviceService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceServiceList. +func (in *DeviceServiceList) DeepCopy() *DeviceServiceList { + if in == nil { + return nil + } + out := new(DeviceServiceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeviceServiceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceServiceSpec) DeepCopyInto(out *DeviceServiceSpec) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceServiceSpec. +func (in *DeviceServiceSpec) DeepCopy() *DeviceServiceSpec { + if in == nil { + return nil + } + out := new(DeviceServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceServiceStatus) DeepCopyInto(out *DeviceServiceStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]DeviceServiceCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceServiceStatus. +func (in *DeviceServiceStatus) DeepCopy() *DeviceServiceStatus { + if in == nil { + return nil + } + out := new(DeviceServiceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceSpec) DeepCopyInto(out *DeviceSpec) { + *out = *in + if in.Protocols != nil { + in, out := &in.Protocols, &out.Protocols + *out = make(map[string]ProtocolProperties, len(*in)) + for key, val := range *in { + var outVal map[string]string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(ProtocolProperties, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + (*out)[key] = outVal + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DeviceProperties != nil { + in, out := &in.DeviceProperties, &out.DeviceProperties + *out = make(map[string]DesiredPropertyState, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceSpec. +func (in *DeviceSpec) DeepCopy() *DeviceSpec { + if in == nil { + return nil + } + out := new(DeviceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeviceStatus) DeepCopyInto(out *DeviceStatus) { + *out = *in + if in.DeviceProperties != nil { + in, out := &in.DeviceProperties, &out.DeviceProperties + *out = make(map[string]ActualPropertyState, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]DeviceCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceStatus. +func (in *DeviceStatus) DeepCopy() *DeviceStatus { + if in == nil { + return nil + } + out := new(DeviceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PlatformAdmin) DeepCopyInto(out *PlatformAdmin) { *out = *in @@ -168,6 +616,64 @@ func (in *PlatformAdminStatus) DeepCopy() *PlatformAdminStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ProtocolProperties) DeepCopyInto(out *ProtocolProperties) { + { + in := &in + *out = make(ProtocolProperties, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProtocolProperties. +func (in ProtocolProperties) DeepCopy() ProtocolProperties { + if in == nil { + return nil + } + out := new(ProtocolProperties) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceOperation) DeepCopyInto(out *ResourceOperation) { + *out = *in + if in.Mappings != nil { + in, out := &in.Mappings, &out.Mappings + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceOperation. +func (in *ResourceOperation) DeepCopy() *ResourceOperation { + if in == nil { + return nil + } + out := new(ResourceOperation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceProperties) DeepCopyInto(out *ResourceProperties) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceProperties. +func (in *ResourceProperties) DeepCopy() *ResourceProperties { + if in == nil { + return nil + } + out := new(ResourceProperties) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceTemplateSpec) DeepCopyInto(out *ServiceTemplateSpec) { *out = *in diff --git a/pkg/controller/platformadmin/platformadmin_controller.go b/pkg/controller/platformadmin/platformadmin_controller.go index 542c147c0f2..dddb4178a9e 100644 --- a/pkg/controller/platformadmin/platformadmin_controller.go +++ b/pkg/controller/platformadmin/platformadmin_controller.go @@ -729,6 +729,12 @@ func (r *ReconcilePlatformAdmin) initFramework(ctx context.Context, platformAdmi platformAdminFramework.Components = r.Configration.NoSectyComponents[platformAdmin.Spec.Version] } + yurtIotDock, err := NewYurtIoTDockComponent(platformAdmin, platformAdminFramework) + if err != nil { + return err + } + platformAdminFramework.Components = append(platformAdminFramework.Components, yurtIotDock) + // For better serialization, the serialization method of the Kubernetes runtime library is used data, err := runtime.Encode(r.yamlSerializer, platformAdminFramework) if err != nil { diff --git a/pkg/controller/platformadmin/util.go b/pkg/controller/platformadmin/util.go new file mode 100644 index 00000000000..6abc2b5e780 --- /dev/null +++ b/pkg/controller/platformadmin/util.go @@ -0,0 +1,126 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package platformadmin + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" + + iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" + "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" + utils "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" +) + +// NewYurtIoTDockComponent initialize the configuration of yurt-iot-dock component +func NewYurtIoTDockComponent(platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminFramework *PlatformAdminFramework) (*config.Component, error) { + var yurtIotDockComponent config.Component + + // If the configuration of the yurt-iot-dock component that customized in the platformAdminFramework + for _, cp := range platformAdminFramework.Components { + if cp.Name != utils.IotDockName { + continue + } + return cp, nil + } + + // Otherwise, the default configuration is used to start + ver, ns, err := utils.DefaultVersion(platformAdmin) + if err != nil { + return nil, err + } + + yurtIotDockComponent.Name = utils.IotDockName + yurtIotDockComponent.Deployment = &appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": utils.IotDockName, + "control-plane": utils.IotDockControlPlane, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": utils.IotDockName, + "control-plane": utils.IotDockControlPlane, + }, + Namespace: ns, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: utils.IotDockName, + Image: fmt.Sprintf("%s:%s", utils.IotDockImage, ver), + ImagePullPolicy: corev1.PullIfNotPresent, + Args: []string{ + "--health-probe-bind-address=:8081", + "--metrics-bind-address=127.0.0.1:8080", + "--leader-elect=false", + fmt.Sprintf("--namespace=%s", ns), + }, + LivenessProbe: &corev1.Probe{ + InitialDelaySeconds: 15, + PeriodSeconds: 20, + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/healthz", + Port: intstr.FromInt(8081), + }, + }, + }, + ReadinessProbe: &corev1.Probe{ + InitialDelaySeconds: 5, + PeriodSeconds: 10, + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/readyz", + Port: intstr.FromInt(8081), + }, + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("512m"), + corev1.ResourceMemory: resource.MustParse("256Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1024m"), + corev1.ResourceMemory: resource.MustParse("512Mi"), + }, + }, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: pointer.Bool(false), + }, + }, + }, + TerminationGracePeriodSeconds: pointer.Int64(10), + SecurityContext: &corev1.PodSecurityContext{ + RunAsUser: pointer.Int64(65532), + }, + }, + }, + } + // YurtIoTDock doesn't need a service yet + yurtIotDockComponent.Service = nil + + return &yurtIotDockComponent, nil +} diff --git a/pkg/controller/platformadmin/utils/util.go b/pkg/controller/platformadmin/utils/conditions.go similarity index 100% rename from pkg/controller/platformadmin/utils/util.go rename to pkg/controller/platformadmin/utils/conditions.go diff --git a/pkg/controller/platformadmin/utils/version.go b/pkg/controller/platformadmin/utils/version.go new file mode 100644 index 00000000000..08542f4c97c --- /dev/null +++ b/pkg/controller/platformadmin/utils/version.go @@ -0,0 +1,31 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" +) + +const IotDockName = "yurt-iot-dock" +const IotDockImage = "openyurt/yurt-iot-dock" +const IotDockControlPlane = "platformadmin-controller" + +func DefaultVersion(platformAdmin *iotv1alpha2.PlatformAdmin) (string, string, error) { + version := "latest" + ns := platformAdmin.Namespace + return version, ns, nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/device_client.go b/pkg/yurtiotdock/clients/edgex-foundry/device_client.go new file mode 100644 index 00000000000..72ef381d8fa --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/device_client.go @@ -0,0 +1,371 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/cookiejar" + "strings" + "time" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/go-resty/resty/v2" + "golang.org/x/net/publicsuffix" + "k8s.io/klog/v2" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceClient struct { + *resty.Client + CoreMetaAddr string + CoreCommandAddr string +} + +func NewEdgexDeviceClient(coreMetaAddr, coreCommandAddr string) *EdgexDeviceClient { + cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + instance := resty.NewWithClient(&http.Client{ + Jar: cookieJar, + Timeout: 10 * time.Second, + }) + return &EdgexDeviceClient{ + Client: instance, + CoreMetaAddr: coreMetaAddr, + CoreCommandAddr: coreCommandAddr, + } +} + +// Create function sends a POST request to EdgeX to add a new device +func (efc *EdgexDeviceClient) Create(ctx context.Context, device *iotv1alpha1.Device, options clients.CreateOptions) (*iotv1alpha1.Device, error) { + devs := []*iotv1alpha1.Device{device} + req := makeEdgeXDeviceRequest(devs) + klog.V(5).Infof("will add the Device: %s", device.Name) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postPath := fmt.Sprintf("http://%s%s", efc.CoreMetaAddr, DevicePath) + resp, err := efc.R(). + SetBody(reqBody).Post(postPath) + if err != nil { + return nil, err + } else if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create device on edgex foundry failed, the response is : %s", resp.Body()) + } + + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDevice := device.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDevice.Status.EdgeId = edgexResps[0].Id + createdDevice.Status.Synced = true + } else { + return nil, fmt.Errorf("create device on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch device cound, the response is : %s", resp.Body()) + } + return createdDevice, err +} + +// Delete function sends a request to EdgeX to delete a device +func (efc *EdgexDeviceClient) Delete(ctx context.Context, name string, options clients.DeleteOptions) error { + klog.V(5).Infof("will delete the Device: %s", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreMetaAddr, DevicePath, name) + resp, err := efc.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return errors.New(string(resp.Body())) + } + return nil +} + +// Update is used to set the admin or operating state of the device by unique name of the device. +// TODO support to update other fields +func (efc *EdgexDeviceClient) Update(ctx context.Context, device *iotv1alpha1.Device, options clients.UpdateOptions) (*iotv1alpha1.Device, error) { + actualDeviceName := getEdgeXName(device) + patchURL := fmt.Sprintf("http://%s%s", efc.CoreMetaAddr, DevicePath) + if device == nil { + return nil, nil + } + devs := []*iotv1alpha1.Device{device} + req := makeEdgeXDeviceUpdateRequest(devs) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + rep, err := efc.R(). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + Patch(patchURL) + if err != nil { + return nil, err + } else if rep.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("failed to update device: %s, get response: %s", actualDeviceName, string(rep.Body())) + } + return device, nil +} + +// Get is used to query the device information corresponding to the device name +func (efc *EdgexDeviceClient) Get(ctx context.Context, deviceName string, options clients.GetOptions) (*iotv1alpha1.Device, error) { + klog.V(5).Infof("will get Devices: %s", deviceName) + var dResp edgex_resp.DeviceResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreMetaAddr, DevicePath, deviceName) + resp, err := efc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("Device %s not found", deviceName) + } + err = json.Unmarshal(resp.Body(), &dResp) + if err != nil { + return nil, err + } + device := toKubeDevice(dResp.Device, options.Namespace) + return &device, err +} + +// List is used to get all device objects on edge platform +// TODO:support label filtering according to options +func (efc *EdgexDeviceClient) List(ctx context.Context, options clients.ListOptions) ([]iotv1alpha1.Device, error) { + lp := fmt.Sprintf("http://%s%s/all?limit=-1", efc.CoreMetaAddr, DevicePath) + resp, err := efc.R().EnableTrace().Get(lp) + if err != nil { + return nil, err + } + var mdResp edgex_resp.MultiDevicesResponse + if err := json.Unmarshal(resp.Body(), &mdResp); err != nil { + return nil, err + } + var res []iotv1alpha1.Device + for _, dp := range mdResp.Devices { + res = append(res, toKubeDevice(dp, options.Namespace)) + } + return res, nil +} + +func (efc *EdgexDeviceClient) GetPropertyState(ctx context.Context, propertyName string, d *iotv1alpha1.Device, options clients.GetOptions) (*iotv1alpha1.ActualPropertyState, error) { + actualDeviceName := getEdgeXName(d) + // get the old property from status + oldAps, exist := d.Status.DeviceProperties[propertyName] + propertyGetURL := "" + // 1. query the Get URL of a property + if !exist || (exist && oldAps.GetURL == "") { + coreCommands, err := efc.GetCommandResponseByName(actualDeviceName) + if err != nil { + return &iotv1alpha1.ActualPropertyState{}, err + } + for _, c := range coreCommands { + if c.Name == propertyName && c.Get { + propertyGetURL = fmt.Sprintf("%s%s", c.Url, c.Path) + break + } + } + if propertyGetURL == "" { + return nil, &clients.NotFoundError{} + } + } else { + propertyGetURL = oldAps.GetURL + } + // 2. get the actual property value by the getURL + actualPropertyState := iotv1alpha1.ActualPropertyState{ + Name: propertyName, + GetURL: propertyGetURL, + } + if resp, err := efc.getPropertyState(propertyGetURL); err != nil { + return nil, err + } else { + var eResp edgex_resp.EventResponse + if err := json.Unmarshal(resp.Body(), &eResp); err != nil { + return nil, err + } + actualPropertyState.ActualValue = getPropertyValueFromEvent(propertyName, eResp.Event) + } + return &actualPropertyState, nil +} + +// getPropertyState returns different error messages according to the status code +func (efc *EdgexDeviceClient) getPropertyState(getURL string) (*resty.Response, error) { + resp, err := efc.R().Get(getURL) + if err != nil { + return resp, err + } + if resp.StatusCode() == 400 { + err = errors.New("request is in an invalid state") + } else if resp.StatusCode() == 404 { + err = errors.New("the requested resource does not exist") + } else if resp.StatusCode() == 423 { + err = errors.New("the device is locked (AdminState) or down (OperatingState)") + } else if resp.StatusCode() == 500 { + err = errors.New("an unexpected error occurred on the server") + } + return resp, err +} + +func (efc *EdgexDeviceClient) UpdatePropertyState(ctx context.Context, propertyName string, d *iotv1alpha1.Device, options clients.UpdateOptions) error { + // Get the actual device name + acturalDeviceName := getEdgeXName(d) + + dps := d.Spec.DeviceProperties[propertyName] + parameterName := dps.Name + if dps.PutURL == "" { + putCmd, err := efc.getPropertyPut(acturalDeviceName, dps.Name) + if err != nil { + return err + } + dps.PutURL = fmt.Sprintf("%s%s", putCmd.Url, putCmd.Path) + if len(putCmd.Parameters) == 1 { + parameterName = putCmd.Parameters[0].ResourceName + } + } + // set the device property to desired state + bodyMap := make(map[string]string) + bodyMap[parameterName] = dps.DesiredValue + body, _ := json.Marshal(bodyMap) + klog.V(5).Infof("setting the property to desired value", "propertyName", parameterName, "desiredValue", string(body)) + rep, err := efc.R(). + SetHeader("Content-Type", "application/json"). + SetBody(body). + Put(dps.PutURL) + if err != nil { + return err + } else if rep.StatusCode() != http.StatusOK { + return fmt.Errorf("failed to set property: %s, get response: %s", dps.Name, string(rep.Body())) + } else if rep.Body() != nil { + // If the parameters are illegal, such as out of range, the 200 status code is also returned, but the description appears in the body + a := string(rep.Body()) + if strings.Contains(a, "execWriteCmd") { + return fmt.Errorf("failed to set property: %s, get response: %s", dps.Name, string(rep.Body())) + } + } + return nil +} + +// Gets the models.Put from edgex foundry which is used to set the device property's value +func (efc *EdgexDeviceClient) getPropertyPut(deviceName, cmdName string) (dtos.CoreCommand, error) { + coreCommands, err := efc.GetCommandResponseByName(deviceName) + if err != nil { + return dtos.CoreCommand{}, err + } + for _, c := range coreCommands { + if cmdName == c.Name && c.Set { + return c, nil + } + } + return dtos.CoreCommand{}, errors.New("corresponding command is not found") +} + +// ListPropertiesState gets all the actual property information about a device +func (efc *EdgexDeviceClient) ListPropertiesState(ctx context.Context, device *iotv1alpha1.Device, options clients.ListOptions) (map[string]iotv1alpha1.DesiredPropertyState, map[string]iotv1alpha1.ActualPropertyState, error) { + actualDeviceName := getEdgeXName(device) + + dpsm := map[string]iotv1alpha1.DesiredPropertyState{} + apsm := map[string]iotv1alpha1.ActualPropertyState{} + coreCommands, err := efc.GetCommandResponseByName(actualDeviceName) + if err != nil { + return dpsm, apsm, err + } + + for _, c := range coreCommands { + // DesiredPropertyState only store the basic information and does not set DesiredValue + if c.Get { + getURL := fmt.Sprintf("%s%s", c.Url, c.Path) + aps, ok := apsm[c.Name] + if ok { + aps.GetURL = getURL + } else { + aps = iotv1alpha1.ActualPropertyState{Name: c.Name, GetURL: getURL} + } + apsm[c.Name] = aps + resp, err := efc.getPropertyState(getURL) + if err != nil { + klog.V(5).ErrorS(err, "getPropertyState failed", "propertyName", c.Name, "deviceName", actualDeviceName) + } else { + var eResp edgex_resp.EventResponse + if err := json.Unmarshal(resp.Body(), &eResp); err != nil { + klog.V(5).ErrorS(err, "failed to decode the response ", "response", resp) + continue + } + event := eResp.Event + readingName := c.Name + expectParams := c.Parameters + if len(expectParams) == 1 { + readingName = expectParams[0].ResourceName + } + klog.V(5).Infof("get reading name %s for command %s of device %s", readingName, c.Name, device.Name) + actualValue := getPropertyValueFromEvent(readingName, event) + aps.ActualValue = actualValue + apsm[c.Name] = aps + } + } + } + return dpsm, apsm, nil +} + +// The actual property value is resolved from the returned event +func getPropertyValueFromEvent(resName string, event dtos.Event) string { + actualValue := "" + for _, r := range event.Readings { + if resName == r.ResourceName { + if r.SimpleReading.Value != "" { + actualValue = r.SimpleReading.Value + } else if len(r.BinaryReading.BinaryValue) != 0 { + // TODO: how to demonstrate binary data + actualValue = fmt.Sprintf("%s:%s", r.BinaryReading.MediaType, "blob value") + } else if r.ObjectReading.ObjectValue != nil { + serializedBytes, _ := json.Marshal(r.ObjectReading.ObjectValue) + actualValue = string(serializedBytes) + } + break + } + } + return actualValue +} + +// GetCommandResponseByName gets all commands supported by the device +func (efc *EdgexDeviceClient) GetCommandResponseByName(deviceName string) ([]dtos.CoreCommand, error) { + klog.V(5).Infof("will get CommandResponses of device: %s", deviceName) + + var dcr edgex_resp.DeviceCoreCommandResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreCommandAddr, CommandResponsePath, deviceName) + + resp, err := efc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, errors.New("Item not found") + } + err = json.Unmarshal(resp.Body(), &dcr) + if err != nil { + return nil, err + } + return dcr.DeviceCoreCommand.CoreCommands, nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go new file mode 100644 index 00000000000..5873a540d74 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go @@ -0,0 +1,202 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package edgex_foundry + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceListMetadata = `{"apiVersion":"v2","statusCode":200,"totalCount":5,"devices":[{"created":1661829206505,"modified":1661829206505,"id":"f6255845-f4b2-4182-bd3c-abc9eac4a649","name":"Random-Float-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Float-Device","autoEvents":[{"interval":"30s","onChange":false,"sourceName":"Float32"},{"interval":"30s","onChange":false,"sourceName":"Float64"}],"protocols":{"other":{"Address":"device-virtual-float-01","Protocol":"300"}}},{"created":1661829206506,"modified":1661829206506,"id":"d29efe20-fdec-4aeb-90e5-99528cb6ca28","name":"Random-Binary-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Binary-Device","protocols":{"other":{"Address":"device-virtual-binary-01","Port":"300"}}},{"created":1661829206504,"modified":1661829206504,"id":"6a7f00a4-9536-48b2-9380-a9fc202ac517","name":"Random-Integer-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Integer-Device","autoEvents":[{"interval":"15s","onChange":false,"sourceName":"Int8"},{"interval":"15s","onChange":false,"sourceName":"Int16"},{"interval":"15s","onChange":false,"sourceName":"Int32"},{"interval":"15s","onChange":false,"sourceName":"Int64"}],"protocols":{"other":{"Address":"device-virtual-int-01","Protocol":"300"}}},{"created":1661829206503,"modified":1661829206503,"id":"439d47a2-fa72-4c27-9f47-c19356cc0c3b","name":"Random-Boolean-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Boolean-Device","autoEvents":[{"interval":"10s","onChange":false,"sourceName":"Bool"}],"protocols":{"other":{"Address":"device-virtual-bool-01","Port":"300"}}},{"created":1661829206505,"modified":1661829206505,"id":"2890ab86-3ae4-4b5e-98ab-aad85fc540e6","name":"Random-UnsignedInteger-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-UnsignedInteger-Device","autoEvents":[{"interval":"20s","onChange":false,"sourceName":"Uint8"},{"interval":"20s","onChange":false,"sourceName":"Uint16"},{"interval":"20s","onChange":false,"sourceName":"Uint32"},{"interval":"20s","onChange":false,"sourceName":"Uint64"}],"protocols":{"other":{"Address":"device-virtual-uint-01","Protocol":"300"}}}]}` + DeviceMetadata = `{"apiVersion":"v2","statusCode":200,"device":{"created":1661829206505,"modified":1661829206505,"id":"f6255845-f4b2-4182-bd3c-abc9eac4a649","name":"Random-Float-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Float-Device","autoEvents":[{"interval":"30s","onChange":false,"sourceName":"Float32"},{"interval":"30s","onChange":false,"sourceName":"Float64"}],"protocols":{"other":{"Address":"device-virtual-float-01","Protocol":"300"}}}}` + + DeviceCreateSuccess = `[{"apiVersion":"v2","statusCode":201,"id":"2fff4f1a-7110-442f-b347-9f896338ba57"}]` + DeviceCreateFail = `[{"apiVersion":"v2","message":"device name test-Random-Float-Device already exists","statusCode":409}]` + + DeviceDeleteSuccess = `{"apiVersion":"v2","statusCode":200}` + DeviceDeleteFail = `{"apiVersion":"v2","message":"fail to query device by name test-Random-Float-Device","statusCode":404}` + + DeviceCoreCommands = `{"apiVersion":"v2","statusCode":200,"deviceCoreCommand":{"deviceName":"Random-Float-Device","profileName":"Random-Float-Device","coreCommands":[{"name":"WriteFloat32ArrayValue","set":true,"path":"/api/v2/device/name/Random-Float-Device/WriteFloat32ArrayValue","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32Array","valueType":"Float32Array"},{"resourceName":"EnableRandomization_Float32Array","valueType":"Bool"}]},{"name":"WriteFloat64ArrayValue","set":true,"path":"/api/v2/device/name/Random-Float-Device/WriteFloat64ArrayValue","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64Array","valueType":"Float64Array"},{"resourceName":"EnableRandomization_Float64Array","valueType":"Bool"}]},{"name":"Float32","get":true,"set":true,"path":"/api/v2/device/name/Random-Float-Device/Float32","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32","valueType":"Float32"}]},{"name":"Float64","get":true,"set":true,"path":"/api/v2/device/name/Random-Float-Device/Float64","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64","valueType":"Float64"}]},{"name":"Float32Array","get":true,"set":true,"path":"/api/v2/device/name/Random-Float-Device/Float32Array","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32Array","valueType":"Float32Array"}]},{"name":"Float64Array","get":true,"set":true,"path":"/api/v2/device/name/Random-Float-Device/Float64Array","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64Array","valueType":"Float64Array"}]},{"name":"WriteFloat32Value","set":true,"path":"/api/v2/device/name/Random-Float-Device/WriteFloat32Value","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32","valueType":"Float32"},{"resourceName":"EnableRandomization_Float32","valueType":"Bool"}]},{"name":"WriteFloat64Value","set":true,"path":"/api/v2/device/name/Random-Float-Device/WriteFloat64Value","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64","valueType":"Float64"},{"resourceName":"EnableRandomization_Float64","valueType":"Bool"}]}]}}` + DeviceCommandResp = `{"apiVersion":"v2","statusCode":200,"event":{"apiVersion":"v2","id":"095090e4-de39-45a1-a0fa-18bc340104e6","deviceName":"Random-Float-Device","profileName":"Random-Float-Device","sourceName":"Float32","origin":1661851070562067780,"readings":[{"id":"972bf6be-3b01-49fc-b211-a43ed51d207d","origin":1661851070562067780,"deviceName":"Random-Float-Device","resourceName":"Float32","profileName":"Random-Float-Device","valueType":"Float32","value":"-2.038811e+38"}]}}` + + DeviceUpdateSuccess = `[{"apiVersion":"v2","statusCode":200}] ` + + DeviceUpdateProperty = `{"apiVersion":"v2","statusCode":200}` +) + +var deviceClient = NewEdgexDeviceClient("edgex-core-metadata:59881", "edgex-core-command:59882") + +func Test_Get(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceMetadata)) + + device, err := deviceClient.Get(context.TODO(), "Random-Float-Device", clients.GetOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, "Random-Float-Device", device.Spec.Profile) +} + +func Test_List(t *testing.T) { + + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/device/all?limit=-1", + httpmock.NewStringResponder(200, DeviceListMetadata)) + + devices, err := deviceClient.List(context.TODO(), clients.ListOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, len(devices), 5) +} + +func Test_Create(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/device", + httpmock.NewStringResponder(207, DeviceCreateSuccess)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Name = "test-Random-Float-Device" + + create, err := deviceClient.Create(context.TODO(), &device, clients.CreateOptions{}) + assert.Nil(t, err) + + assert.Equal(t, "test-Random-Float-Device", create.Name) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/device", + httpmock.NewStringResponder(207, DeviceCreateFail)) + + create, err = deviceClient.Create(context.TODO(), &device, clients.CreateOptions{}) + assert.NotNil(t, err) + assert.Nil(t, create) +} + +func Test_Delete(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/device/name/test-Random-Float-Device", + httpmock.NewStringResponder(200, DeviceDeleteSuccess)) + + err := deviceClient.Delete(context.TODO(), "test-Random-Float-Device", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/device/name/test-Random-Float-Device", + httpmock.NewStringResponder(404, DeviceDeleteFail)) + + err = deviceClient.Delete(context.TODO(), "test-Random-Float-Device", clients.DeleteOptions{}) + assert.NotNil(t, err) +} + +func Test_GetPropertyState(t *testing.T) { + + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device/Float32", + httpmock.NewStringResponder(200, DeviceCommandResp)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + + _, err = deviceClient.GetPropertyState(context.TODO(), "Float32", &device, clients.GetOptions{}) + assert.Nil(t, err) +} + +func Test_ListPropertiesState(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + + _, _, err = deviceClient.ListPropertiesState(context.TODO(), &device, clients.ListOptions{}) + assert.Nil(t, err) +} + +func Test_UpdateDevice(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v2/device", + httpmock.NewStringResponder(207, DeviceUpdateSuccess)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Spec.AdminState = "LOCKED" + + _, err = deviceClient.Update(context.TODO(), &device, clients.UpdateOptions{}) + assert.Nil(t, err) +} + +func Test_UpdatePropertyState(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + + httpmock.RegisterResponder("PUT", "http://edgex-core-command:59882/api/v2/device/name/Random-Float-Device/Float32", + httpmock.NewStringResponder(200, DeviceUpdateSuccess)) + var resp edgex_resp.DeviceResponse + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Spec.DeviceProperties = map[string]iotv1alpha1.DesiredPropertyState{ + "Float32": { + Name: "Float32", + DesiredValue: "66.66", + }, + } + + err = deviceClient.UpdatePropertyState(context.TODO(), "Float32", &device, clients.UpdateOptions{}) + assert.Nil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go b/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go new file mode 100644 index 00000000000..2cd9c9d40c6 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go @@ -0,0 +1,141 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/go-resty/resty/v2" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + devcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceProfile struct { + *resty.Client + CoreMetaAddr string +} + +func NewEdgexDeviceProfile(coreMetaAddr string) *EdgexDeviceProfile { + return &EdgexDeviceProfile{ + Client: resty.New(), + CoreMetaAddr: coreMetaAddr, + } +} + +// TODO: support label filtering +func getListDeviceProfileURL(address string, opts devcli.ListOptions) (string, error) { + url := fmt.Sprintf("http://%s%s/all?limit=-1", address, DeviceProfilePath) + return url, nil +} + +func (cdc *EdgexDeviceProfile) List(ctx context.Context, opts devcli.ListOptions) ([]v1alpha1.DeviceProfile, error) { + klog.V(5).Info("will list DeviceProfiles") + lp, err := getListDeviceProfileURL(cdc.CoreMetaAddr, opts) + if err != nil { + return nil, err + } + resp, err := cdc.R().EnableTrace().Get(lp) + if err != nil { + return nil, err + } + var mdpResp responses.MultiDeviceProfilesResponse + if err := json.Unmarshal(resp.Body(), &mdpResp); err != nil { + return nil, err + } + var deviceProfiles []v1alpha1.DeviceProfile + for _, dp := range mdpResp.Profiles { + deviceProfiles = append(deviceProfiles, toKubeDeviceProfile(&dp, opts.Namespace)) + } + return deviceProfiles, nil +} + +func (cdc *EdgexDeviceProfile) Get(ctx context.Context, name string, opts devcli.GetOptions) (*v1alpha1.DeviceProfile, error) { + klog.V(5).Infof("will get DeviceProfiles: %s", name) + var dpResp responses.DeviceProfileResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", cdc.CoreMetaAddr, DeviceProfilePath, name) + resp, err := cdc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("DeviceProfile %s not found", name) + } + if err = json.Unmarshal(resp.Body(), &dpResp); err != nil { + return nil, err + } + kubedp := toKubeDeviceProfile(&dpResp.Profile, opts.Namespace) + return &kubedp, nil +} + +func (cdc *EdgexDeviceProfile) Create(ctx context.Context, deviceProfile *v1alpha1.DeviceProfile, opts devcli.CreateOptions) (*v1alpha1.DeviceProfile, error) { + dps := []*v1alpha1.DeviceProfile{deviceProfile} + req := makeEdgeXDeviceProfilesRequest(dps) + klog.V(5).Infof("will add the DeviceProfile: %s", deviceProfile.Name) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postURL := fmt.Sprintf("http://%s%s", cdc.CoreMetaAddr, DeviceProfilePath) + resp, err := cdc.R().SetBody(reqBody).Post(postURL) + if err != nil { + return nil, err + } + if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create edgex deviceProfile err: %s", string(resp.Body())) // 假定 resp.Body() 存了 msg 信息 + } + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDeviceProfile := deviceProfile.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDeviceProfile.Status.EdgeId = edgexResps[0].Id + createdDeviceProfile.Status.Synced = true + } else { + return nil, fmt.Errorf("create deviceprofile on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch DeviceProfile count, the response is : %s", resp.Body()) + } + return createdDeviceProfile, err +} + +// TODO: edgex does not support update DeviceProfile +func (cdc *EdgexDeviceProfile) Update(ctx context.Context, deviceProfile *v1alpha1.DeviceProfile, opts devcli.UpdateOptions) (*v1alpha1.DeviceProfile, error) { + return nil, nil +} + +func (cdc *EdgexDeviceProfile) Delete(ctx context.Context, name string, opts devcli.DeleteOptions) error { + klog.V(5).Infof("will delete the DeviceProfile: %s", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", cdc.CoreMetaAddr, DeviceProfilePath, name) + resp, err := cdc.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return fmt.Errorf("delete edgex deviceProfile err: %s", string(resp.Body())) // 假定 resp.Body() 存了 msg 信息 + } + return nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go new file mode 100644 index 00000000000..0f32eecf687 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package edgex_foundry + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceProfileListMetaData = `{"apiVersion":"v2","statusCode":200,"totalCount":5,"profiles":[{"created":1661829206499,"modified":1661829206499,"id":"cf624c1f-c93a-48c0-b327-b00c7dc171f1","name":"Random-Binary-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"Generate random binary value","name":"Binary","isHidden":false,"tag":"","properties":{"valueType":"Binary","readWrite":"R","units":"","minimum":"","maximum":"","defaultValue":"","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":"random"},"attributes":null}],"deviceCommands":[]},{"created":1661829206501,"modified":1661829206501,"id":"adeafefa-2d11-4eee-8fe9-a4742f85f7fb","name":"Random-UnsignedInteger-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint8","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint16","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint8 value","name":"Uint8","isHidden":false,"tag":"","properties":{"valueType":"Uint8","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint16 value","name":"Uint16","isHidden":false,"tag":"","properties":{"valueType":"Uint16","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint32 value","name":"Uint32","isHidden":false,"tag":"","properties":{"valueType":"Uint32","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint64 value","name":"Uint64","isHidden":false,"tag":"","properties":{"valueType":"Uint64","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint8Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint16Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint8 array value","name":"Uint8Array","isHidden":false,"tag":"","properties":{"valueType":"Uint8Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint16 array value","name":"Uint16Array","isHidden":false,"tag":"","properties":{"valueType":"Uint16Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint32 array value","name":"Uint32Array","isHidden":false,"tag":"","properties":{"valueType":"Uint32Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint64 array value","name":"Uint64Array","isHidden":false,"tag":"","properties":{"valueType":"Uint64Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteUint8Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint8","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint8","defaultValue":"false","mappings":null}]},{"name":"WriteUint16Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint16","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint16","defaultValue":"false","mappings":null}]},{"name":"WriteUint32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint32","defaultValue":"false","mappings":null}]},{"name":"WriteUint64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint64","defaultValue":"false","mappings":null}]},{"name":"WriteUint8ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint8Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint8Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint16ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint16Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint16Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint32Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206500,"modified":1661829206500,"id":"67f4a5a1-06e6-4051-b71d-655ec5dd4eb2","name":"Random-Integer-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int8","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int16","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int8 value","name":"Int8","isHidden":false,"tag":"","properties":{"valueType":"Int8","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int16 value","name":"Int16","isHidden":false,"tag":"","properties":{"valueType":"Int16","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int32 value","name":"Int32","isHidden":false,"tag":"","properties":{"valueType":"Int32","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int64 value","name":"Int64","isHidden":false,"tag":"","properties":{"valueType":"Int64","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int8Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int16Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int8 array value","name":"Int8Array","isHidden":false,"tag":"","properties":{"valueType":"Int8Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int16 array value","name":"Int16Array","isHidden":false,"tag":"","properties":{"valueType":"Int16Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int32 array value","name":"Int32Array","isHidden":false,"tag":"","properties":{"valueType":"Int32Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int64 array value","name":"Int64Array","isHidden":false,"tag":"","properties":{"valueType":"Int64Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteInt8Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int8","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int8","defaultValue":"false","mappings":null}]},{"name":"WriteInt16Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int16","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int16","defaultValue":"false","mappings":null}]},{"name":"WriteInt32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int32","defaultValue":"false","mappings":null}]},{"name":"WriteInt64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int64","defaultValue":"false","mappings":null}]},{"name":"WriteInt8ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int8Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int8Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt16ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int16Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int16Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int32Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206500,"modified":1661829206500,"id":"30b8448f-0532-44fb-aed7-5fe4bca16f9a","name":"Random-Float-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float32 value","name":"Float32","isHidden":false,"tag":"","properties":{"valueType":"Float32","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float64 value","name":"Float64","isHidden":false,"tag":"","properties":{"valueType":"Float64","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"0","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float32 array value","name":"Float32Array","isHidden":false,"tag":"","properties":{"valueType":"Float32Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float64 array value","name":"Float64Array","isHidden":false,"tag":"","properties":{"valueType":"Float64Array","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[0]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteFloat32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float32","defaultValue":"false","mappings":null}]},{"name":"WriteFloat64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float64","defaultValue":"false","mappings":null}]},{"name":"WriteFloat32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float32Array","defaultValue":"false","mappings":null}]},{"name":"WriteFloat64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206499,"modified":1661829206499,"id":"01dfe04d-f361-41fd-b1c4-7ca0718f461a","name":"Random-Boolean-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Bool","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean value","name":"Bool","isHidden":false,"tag":"","properties":{"valueType":"Bool","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_BoolArray","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean array value","name":"BoolArray","isHidden":false,"tag":"","properties":{"valueType":"BoolArray","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[true]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteBoolValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Bool","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Bool","defaultValue":"false","mappings":null}]},{"name":"WriteBoolArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"BoolArray","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_BoolArray","defaultValue":"false","mappings":null}]}]}]}` + DeviceProfileMetaData = `{"apiVersion":"v2","statusCode":200,"profile":{"created":1661829206499,"modified":1661829206499,"id":"01dfe04d-f361-41fd-b1c4-7ca0718f461a","name":"Random-Boolean-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Bool","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean value","name":"Bool","isHidden":false,"tag":"","properties":{"valueType":"Bool","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_BoolArray","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","minimum":"","maximum":"","defaultValue":"true","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean array value","name":"BoolArray","isHidden":false,"tag":"","properties":{"valueType":"BoolArray","readWrite":"RW","units":"","minimum":"","maximum":"","defaultValue":"[true]","mask":"","shift":"","scale":"","offset":"","base":"","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteBoolValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Bool","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Bool","defaultValue":"false","mappings":null}]},{"name":"WriteBoolArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"BoolArray","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_BoolArray","defaultValue":"false","mappings":null}]}]}}` + + ProfileCreateSuccess = `[{"apiVersion":"v2","statusCode":201,"id":"a583b97d-7c4d-4b7c-8b93-51da9e68518c"}]` + ProfileCreateFail = `[{"apiVersion":"v2","message":"device profile name test-Random-Boolean-Device exists","statusCode":409}]` + + ProfileDeleteSuccess = `{"apiVersion":"v2","statusCode":200}` + ProfileDeleteFail = `{"apiVersion":"v2","message":"fail to delete the device profile with name test-Random-Boolean-Device","statusCode":404}` +) + +var profileClient = NewEdgexDeviceProfile("edgex-core-metadata:59881") + +func Test_ListProfile(t *testing.T) { + + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/deviceprofile/all?limit=-1", + httpmock.NewStringResponder(200, DeviceProfileListMetaData)) + profiles, err := profileClient.List(context.TODO(), clients.ListOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, 5, len(profiles)) +} + +func Test_GetProfile(T *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/deviceprofile/name/Random-Boolean-Device", + httpmock.NewStringResponder(200, DeviceProfileMetaData)) + + _, err := profileClient.Get(context.TODO(), "Random-Boolean-Device", clients.GetOptions{Namespace: "default"}) + assert.Nil(T, err) +} + +func Test_CreateProfile(t *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/deviceprofile", + httpmock.NewStringResponder(207, ProfileCreateSuccess)) + + var resp edgex_resp.DeviceProfileResponse + + err := json.Unmarshal([]byte(DeviceProfileMetaData), &resp) + assert.Nil(t, err) + + profile := toKubeDeviceProfile(&resp.Profile, "default") + profile.Name = "test-Random-Boolean-Device" + + _, err = profileClient.Create(context.TODO(), &profile, clients.CreateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/deviceprofile", + httpmock.NewStringResponder(207, ProfileCreateFail)) + + _, err = profileClient.Create(context.TODO(), &profile, clients.CreateOptions{}) + assert.NotNil(t, err) +} + +func Test_DeleteProfile(t *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/deviceprofile/name/test-Random-Boolean-Device", + httpmock.NewStringResponder(200, ProfileDeleteSuccess)) + + err := profileClient.Delete(context.TODO(), "test-Random-Boolean-Device", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/deviceprofile/name/test-Random-Boolean-Device", + httpmock.NewStringResponder(404, ProfileDeleteFail)) + + err = profileClient.Delete(context.TODO(), "test-Random-Boolean-Device", clients.DeleteOptions{}) + assert.NotNil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go b/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go new file mode 100644 index 00000000000..c7cd9f70f24 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go @@ -0,0 +1,166 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/go-resty/resty/v2" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + edgeCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceServiceClient struct { + *resty.Client + CoreMetaAddr string +} + +func NewEdgexDeviceServiceClient(coreMetaAddr string) *EdgexDeviceServiceClient { + return &EdgexDeviceServiceClient{ + Client: resty.New(), + CoreMetaAddr: coreMetaAddr, + } +} + +// Create function sends a POST request to EdgeX to add a new deviceService +func (eds *EdgexDeviceServiceClient) Create(ctx context.Context, deviceService *v1alpha1.DeviceService, options edgeCli.CreateOptions) (*v1alpha1.DeviceService, error) { + dss := []*v1alpha1.DeviceService{deviceService} + req := makeEdgeXDeviceService(dss) + klog.V(5).InfoS("will add the DeviceServices", "DeviceService", deviceService.Name) + jsonBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postPath := fmt.Sprintf("http://%s%s", eds.CoreMetaAddr, DeviceServicePath) + resp, err := eds.R(). + SetBody(jsonBody).Post(postPath) + if err != nil { + return nil, err + } else if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create DeviceService on edgex foundry failed, the response is : %s", resp.Body()) + } + + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDeviceService := deviceService.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDeviceService.Status.EdgeId = edgexResps[0].Id + createdDeviceService.Status.Synced = true + } else { + return nil, fmt.Errorf("create DeviceService on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch DeviceService count, the response is : %s", resp.Body()) + } + return createdDeviceService, err +} + +// Delete function sends a request to EdgeX to delete a deviceService +func (eds *EdgexDeviceServiceClient) Delete(ctx context.Context, name string, option edgeCli.DeleteOptions) error { + klog.V(5).InfoS("will delete the DeviceService", "DeviceService", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", eds.CoreMetaAddr, DeviceServicePath, name) + resp, err := eds.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return fmt.Errorf("delete edgex deviceservice err: %s", string(resp.Body())) + } + return nil +} + +// Update is used to set the admin or operating state of the deviceService by unique name of the deviceService. +// TODO support to update other fields +func (eds *EdgexDeviceServiceClient) Update(ctx context.Context, ds *v1alpha1.DeviceService, options edgeCli.UpdateOptions) (*v1alpha1.DeviceService, error) { + patchURL := fmt.Sprintf("http://%s%s", eds.CoreMetaAddr, DeviceServicePath) + if ds == nil { + return nil, nil + } + + if ds.Status.EdgeId == "" { + return nil, fmt.Errorf("failed to update deviceservice %s with empty edgex id", ds.Name) + } + edgeDs := toEdgexDeviceService(ds) + edgeDs.Id = ds.Status.EdgeId + dsJson, err := json.Marshal(&edgeDs) + if err != nil { + return nil, err + } + resp, err := eds.R(). + SetBody(dsJson).Patch(patchURL) + if err != nil { + return nil, err + } + + if resp.StatusCode() == http.StatusOK || resp.StatusCode() == http.StatusMultiStatus { + return ds, nil + } else { + return nil, fmt.Errorf("request to patch deviceservice failed, errcode:%d", resp.StatusCode()) + } +} + +// Get is used to query the deviceService information corresponding to the deviceService name +func (eds *EdgexDeviceServiceClient) Get(ctx context.Context, name string, options edgeCli.GetOptions) (*v1alpha1.DeviceService, error) { + klog.V(5).InfoS("will get DeviceServices", "DeviceService", name) + var dsResp responses.DeviceServiceResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", eds.CoreMetaAddr, DeviceServicePath, name) + resp, err := eds.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("deviceservice %s not found", name) + } + err = json.Unmarshal(resp.Body(), &dsResp) + if err != nil { + return nil, err + } + ds := toKubeDeviceService(dsResp.Service, options.Namespace) + return &ds, nil +} + +// List is used to get all deviceService objects on edge platform +// The Hanoi version currently supports only a single label and does not support other filters +func (eds *EdgexDeviceServiceClient) List(ctx context.Context, options edgeCli.ListOptions) ([]v1alpha1.DeviceService, error) { + klog.V(5).Info("will list DeviceServices") + lp := fmt.Sprintf("http://%s%s/all?limit=-1", eds.CoreMetaAddr, DeviceServicePath) + resp, err := eds.R(). + EnableTrace(). + Get(lp) + if err != nil { + return nil, err + } + var mdsResponse responses.MultiDeviceServicesResponse + if err := json.Unmarshal(resp.Body(), &mdsResponse); err != nil { + return nil, err + } + var res []v1alpha1.DeviceService + for _, ds := range mdsResponse.Services { + res = append(res, toKubeDeviceService(ds, options.Namespace)) + } + return res, nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go new file mode 100644 index 00000000000..c00d7b9118e --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package edgex_foundry + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceServiceListMetaData = `{"apiVersion":"v2","statusCode":200,"totalCount":1,"services":[{"created":1661829206490,"modified":1661850999190,"id":"74516e96-973d-4cad-bad1-afd4b3a8ea46","name":"device-virtual","baseAddress":"http://edgex-device-virtual:59900","adminState":"UNLOCKED"}]}` + DeviceServiceMetaData = `{"apiVersion":"v2","statusCode":200,"service":{"created":1661829206490,"modified":1661850999190,"id":"74516e96-973d-4cad-bad1-afd4b3a8ea46","name":"device-virtual","baseAddress":"http://edgex-device-virtual:59900","adminState":"UNLOCKED"}}` + ServiceCreateSuccess = `[{"apiVersion":"v2","statusCode":201,"id":"a583b97d-7c4d-4b7c-8b93-51da9e68518c"}]` + ServiceCreateFail = `[{"apiVersion":"v2","message":"device service name test-device-virtual exists","statusCode":409}]` + + ServiceDeleteSuccess = `{"apiVersion":"v2","statusCode":200}` + ServiceDeleteFail = `{"apiVersion":"v2","message":"fail to delete the device profile with name test-Random-Boolean-Device","statusCode":404}` + + ServiceUpdateSuccess = `[{"apiVersion":"v2","statusCode":200}]` + ServiceUpdateFail = `[{"apiVersion":"v2","message":"fail to query object *models.DeviceService, because id: md|ds:01dfe04d-f361-41fd-b1c4-7ca0718f461a doesn't exist in the database","statusCode":404}]` +) + +var serviceClient = NewEdgexDeviceServiceClient("edgex-core-metadata:59881") + +func Test_GetService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/deviceservice/name/device-virtual", + httpmock.NewStringResponder(200, DeviceServiceMetaData)) + + _, err := serviceClient.Get(context.TODO(), "device-virtual", clients.GetOptions{Namespace: "default"}) + assert.Nil(t, err) +} + +func Test_ListService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v2/deviceservice/all?limit=-1", + httpmock.NewStringResponder(200, DeviceServiceListMetaData)) + + services, err := serviceClient.List(context.TODO(), clients.ListOptions{}) + assert.Nil(t, err) + assert.Equal(t, 1, len(services)) +} + +func Test_CreateService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/deviceservice", + httpmock.NewStringResponder(207, ServiceCreateSuccess)) + + var resp edgex_resp.DeviceServiceResponse + + err := json.Unmarshal([]byte(DeviceServiceMetaData), &resp) + assert.Nil(t, err) + + service := toKubeDeviceService(resp.Service, "default") + service.Name = "test-device-virtual" + + _, err = serviceClient.Create(context.TODO(), &service, clients.CreateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v2/deviceservice", + httpmock.NewStringResponder(207, ServiceCreateFail)) +} + +func Test_DeleteService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/deviceservice/name/test-device-virtual", + httpmock.NewStringResponder(200, ServiceDeleteSuccess)) + + err := serviceClient.Delete(context.TODO(), "test-device-virtual", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v2/deviceservice/name/test-device-virtual", + httpmock.NewStringResponder(404, ServiceDeleteFail)) + + err = serviceClient.Delete(context.TODO(), "test-device-virtual", clients.DeleteOptions{}) + assert.NotNil(t, err) +} + +func Test_UpdateService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v2/deviceservice", + httpmock.NewStringResponder(200, ServiceUpdateSuccess)) + var resp edgex_resp.DeviceServiceResponse + + err := json.Unmarshal([]byte(DeviceServiceMetaData), &resp) + assert.Nil(t, err) + + service := toKubeDeviceService(resp.Service, "default") + _, err = serviceClient.Update(context.TODO(), &service, clients.UpdateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v2/deviceservice", + httpmock.NewStringResponder(404, ServiceUpdateFail)) + + _, err = serviceClient.Update(context.TODO(), &service, clients.UpdateOptions{}) + assert.NotNil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go b/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go new file mode 100644 index 00000000000..4a093bf9834 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +type EdgeXObject interface { + IsAddedToEdgeX() bool +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/util.go b/pkg/yurtiotdock/clients/edgex-foundry/util.go new file mode 100644 index 00000000000..8fbf276ab30 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/util.go @@ -0,0 +1,456 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package edgex_foundry + +import ( + "fmt" + "strings" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v2/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v2/models" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +const ( + EdgeXObjectName = "yurt-iot-dock/edgex-object.name" + DeviceServicePath = "/api/v2/deviceservice" + DeviceProfilePath = "/api/v2/deviceprofile" + DevicePath = "/api/v2/device" + CommandResponsePath = "/api/v2/device" + + APIVersionV2 = "v2" +) + +type ClientURL struct { + Host string + Port int +} + +func getEdgeXName(provider metav1.Object) string { + var actualDeviceName string + if _, ok := provider.GetLabels()[EdgeXObjectName]; ok { + actualDeviceName = provider.GetLabels()[EdgeXObjectName] + } else { + actualDeviceName = provider.GetName() + } + return actualDeviceName +} + +func toEdgexDeviceService(ds *iotv1alpha1.DeviceService) dtos.DeviceService { + return dtos.DeviceService{ + Description: ds.Spec.Description, + Name: getEdgeXName(ds), + LastConnected: ds.Status.LastConnected, + LastReported: ds.Status.LastReported, + Labels: ds.Spec.Labels, + AdminState: string(ds.Spec.AdminState), + BaseAddress: ds.Spec.BaseAddress, + } +} + +func toEdgeXDeviceResourceSlice(drs []iotv1alpha1.DeviceResource) []dtos.DeviceResource { + var ret []dtos.DeviceResource + for _, dr := range drs { + ret = append(ret, toEdgeXDeviceResource(dr)) + } + return ret +} + +func toEdgeXDeviceResource(dr iotv1alpha1.DeviceResource) dtos.DeviceResource { + genericAttrs := make(map[string]interface{}) + for k, v := range dr.Attributes { + genericAttrs[k] = v + } + + return dtos.DeviceResource{ + Description: dr.Description, + Name: dr.Name, + Tag: dr.Tag, + Properties: toEdgeXProfileProperty(dr.Properties), + Attributes: genericAttrs, + } +} + +func toEdgeXProfileProperty(pp iotv1alpha1.ResourceProperties) dtos.ResourceProperties { + return dtos.ResourceProperties{ + ReadWrite: pp.ReadWrite, + Minimum: pp.Minimum, + Maximum: pp.Maximum, + DefaultValue: pp.DefaultValue, + Mask: pp.Mask, + Shift: pp.Shift, + Scale: pp.Scale, + Offset: pp.Offset, + Base: pp.Base, + Assertion: pp.Assertion, + MediaType: pp.MediaType, + Units: pp.Units, + ValueType: pp.ValueType, + } +} + +func toKubeDeviceService(ds dtos.DeviceService, namespace string) iotv1alpha1.DeviceService { + return iotv1alpha1.DeviceService{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(ds.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: ds.Name, + }, + }, + Spec: iotv1alpha1.DeviceServiceSpec{ + Description: ds.Description, + Labels: ds.Labels, + AdminState: iotv1alpha1.AdminState(ds.AdminState), + BaseAddress: ds.BaseAddress, + }, + Status: iotv1alpha1.DeviceServiceStatus{ + EdgeId: ds.Id, + LastConnected: ds.LastConnected, + LastReported: ds.LastReported, + AdminState: iotv1alpha1.AdminState(ds.AdminState), + }, + } +} + +func toEdgeXDevice(d *iotv1alpha1.Device) dtos.Device { + md := dtos.Device{ + Description: d.Spec.Description, + Name: getEdgeXName(d), + AdminState: string(toEdgeXAdminState(d.Spec.AdminState)), + OperatingState: string(toEdgeXOperatingState(d.Spec.OperatingState)), + Protocols: toEdgeXProtocols(d.Spec.Protocols), + LastConnected: d.Status.LastConnected, + LastReported: d.Status.LastReported, + Labels: d.Spec.Labels, + Location: d.Spec.Location, + ServiceName: d.Spec.Service, + ProfileName: d.Spec.Profile, + } + if d.Status.EdgeId != "" { + md.Id = d.Status.EdgeId + } + return md +} + +func toEdgeXUpdateDevice(d *iotv1alpha1.Device) dtos.UpdateDevice { + adminState := string(toEdgeXAdminState(d.Spec.AdminState)) + operationState := string(toEdgeXOperatingState(d.Spec.OperatingState)) + md := dtos.UpdateDevice{ + Description: &d.Spec.Description, + Name: &d.Name, + AdminState: &adminState, + OperatingState: &operationState, + Protocols: toEdgeXProtocols(d.Spec.Protocols), + LastConnected: &d.Status.LastConnected, + LastReported: &d.Status.LastReported, + Labels: d.Spec.Labels, + Location: d.Spec.Location, + ServiceName: &d.Spec.Service, + ProfileName: &d.Spec.Profile, + Notify: &d.Spec.Notify, + } + if d.Status.EdgeId != "" { + md.Id = &d.Status.EdgeId + } + return md +} + +func toEdgeXProtocols( + pps map[string]iotv1alpha1.ProtocolProperties) map[string]dtos.ProtocolProperties { + ret := map[string]dtos.ProtocolProperties{} + for k, v := range pps { + ret[k] = dtos.ProtocolProperties(v) + } + return ret +} + +func toEdgeXAdminState(as iotv1alpha1.AdminState) models.AdminState { + if as == iotv1alpha1.Locked { + return models.Locked + } + return models.Unlocked +} + +func toEdgeXOperatingState(os iotv1alpha1.OperatingState) models.OperatingState { + if os == iotv1alpha1.Up { + return models.Up + } else if os == iotv1alpha1.Down { + return models.Down + } + return models.Unknown +} + +// toKubeDevice serialize the EdgeX Device to the corresponding Kubernetes Device +func toKubeDevice(ed dtos.Device, namespace string) iotv1alpha1.Device { + var loc string + if ed.Location != nil { + loc = ed.Location.(string) + } + return iotv1alpha1.Device{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(ed.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: ed.Name, + }, + }, + Spec: iotv1alpha1.DeviceSpec{ + Description: ed.Description, + AdminState: iotv1alpha1.AdminState(ed.AdminState), + OperatingState: iotv1alpha1.OperatingState(ed.OperatingState), + Protocols: toKubeProtocols(ed.Protocols), + Labels: ed.Labels, + Location: loc, + Service: ed.ServiceName, + Profile: ed.ProfileName, + // TODO: Notify + }, + Status: iotv1alpha1.DeviceStatus{ + LastConnected: ed.LastConnected, + LastReported: ed.LastReported, + Synced: true, + EdgeId: ed.Id, + AdminState: iotv1alpha1.AdminState(ed.AdminState), + OperatingState: iotv1alpha1.OperatingState(ed.OperatingState), + }, + } +} + +// toKubeProtocols serialize the EdgeX ProtocolProperties to the corresponding +// Kubernetes OperatingState +func toKubeProtocols( + eps map[string]dtos.ProtocolProperties) map[string]iotv1alpha1.ProtocolProperties { + ret := map[string]iotv1alpha1.ProtocolProperties{} + for k, v := range eps { + ret[k] = iotv1alpha1.ProtocolProperties(v) + } + return ret +} + +// toKubeDeviceProfile create DeviceProfile in cloud according to devicProfile in edge +func toKubeDeviceProfile(dp *dtos.DeviceProfile, namespace string) iotv1alpha1.DeviceProfile { + return iotv1alpha1.DeviceProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(dp.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: dp.Name, + }, + }, + Spec: iotv1alpha1.DeviceProfileSpec{ + Description: dp.Description, + Manufacturer: dp.Manufacturer, + Model: dp.Model, + Labels: dp.Labels, + DeviceResources: toKubeDeviceResources(dp.DeviceResources), + DeviceCommands: toKubeDeviceCommand(dp.DeviceCommands), + }, + Status: iotv1alpha1.DeviceProfileStatus{ + EdgeId: dp.Id, + Synced: true, + }, + } +} + +func toKubeDeviceCommand(dcs []dtos.DeviceCommand) []iotv1alpha1.DeviceCommand { + var ret []iotv1alpha1.DeviceCommand + for _, dc := range dcs { + ret = append(ret, iotv1alpha1.DeviceCommand{ + Name: dc.Name, + ReadWrite: dc.ReadWrite, + IsHidden: dc.IsHidden, + ResourceOperations: toKubeResourceOperations(dc.ResourceOperations), + }) + } + return ret +} + +func toEdgeXDeviceCommand(dcs []iotv1alpha1.DeviceCommand) []dtos.DeviceCommand { + var ret []dtos.DeviceCommand + for _, dc := range dcs { + ret = append(ret, dtos.DeviceCommand{ + Name: dc.Name, + ReadWrite: dc.ReadWrite, + IsHidden: dc.IsHidden, + ResourceOperations: toEdgeXResourceOperations(dc.ResourceOperations), + }) + } + return ret +} + +func toKubeResourceOperations(ros []dtos.ResourceOperation) []iotv1alpha1.ResourceOperation { + var ret []iotv1alpha1.ResourceOperation + for _, ro := range ros { + ret = append(ret, iotv1alpha1.ResourceOperation{ + DeviceResource: ro.DeviceResource, + Mappings: ro.Mappings, + DefaultValue: ro.DefaultValue, + }) + } + return ret +} + +func toEdgeXResourceOperations(ros []iotv1alpha1.ResourceOperation) []dtos.ResourceOperation { + var ret []dtos.ResourceOperation + for _, ro := range ros { + ret = append(ret, dtos.ResourceOperation{ + DeviceResource: ro.DeviceResource, + Mappings: ro.Mappings, + DefaultValue: ro.DefaultValue, + }) + } + return ret +} + +func toKubeDeviceResources(drs []dtos.DeviceResource) []iotv1alpha1.DeviceResource { + var ret []iotv1alpha1.DeviceResource + for _, dr := range drs { + ret = append(ret, toKubeDeviceResource(dr)) + } + return ret +} + +func toKubeDeviceResource(dr dtos.DeviceResource) iotv1alpha1.DeviceResource { + concreteAttrs := make(map[string]string) + for k, v := range dr.Attributes { + switch asserted := v.(type) { + case string: + concreteAttrs[k] = asserted + continue + case int: + concreteAttrs[k] = fmt.Sprintf("%d", asserted) + continue + case float64: + concreteAttrs[k] = fmt.Sprintf("%f", asserted) + continue + case fmt.Stringer: + concreteAttrs[k] = asserted.String() + continue + } + } + + return iotv1alpha1.DeviceResource{ + Description: dr.Description, + Name: dr.Name, + Tag: dr.Tag, + IsHidden: dr.IsHidden, + Properties: toKubeProfileProperty(dr.Properties), + Attributes: concreteAttrs, + } +} + +func toKubeProfileProperty(rp dtos.ResourceProperties) iotv1alpha1.ResourceProperties { + return iotv1alpha1.ResourceProperties{ + ValueType: rp.ValueType, + ReadWrite: rp.ReadWrite, + Minimum: rp.Minimum, + Maximum: rp.Maximum, + DefaultValue: rp.DefaultValue, + Mask: rp.Mask, + Shift: rp.Shift, + Scale: rp.Scale, + Offset: rp.Offset, + Base: rp.Base, + Assertion: rp.Assertion, + MediaType: rp.MediaType, + Units: rp.Units, + } +} + +// toEdgeXDeviceProfile create DeviceProfile in edge according to devicProfile in cloud +func toEdgeXDeviceProfile(dp *iotv1alpha1.DeviceProfile) dtos.DeviceProfile { + return dtos.DeviceProfile{ + DeviceProfileBasicInfo: dtos.DeviceProfileBasicInfo{ + Description: dp.Spec.Description, + Name: getEdgeXName(dp), + Manufacturer: dp.Spec.Manufacturer, + Model: dp.Spec.Model, + Labels: dp.Spec.Labels, + }, + DeviceResources: toEdgeXDeviceResourceSlice(dp.Spec.DeviceResources), + DeviceCommands: toEdgeXDeviceCommand(dp.Spec.DeviceCommands), + } +} + +func makeEdgeXDeviceProfilesRequest(dps []*iotv1alpha1.DeviceProfile) []*requests.DeviceProfileRequest { + var req []*requests.DeviceProfileRequest + for _, dp := range dps { + req = append(req, &requests.DeviceProfileRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV2, + }, + }, + Profile: toEdgeXDeviceProfile(dp), + }) + } + return req +} + +func makeEdgeXDeviceUpdateRequest(devs []*iotv1alpha1.Device) []*requests.UpdateDeviceRequest { + var req []*requests.UpdateDeviceRequest + for _, dev := range devs { + req = append(req, &requests.UpdateDeviceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV2, + }, + }, + Device: toEdgeXUpdateDevice(dev), + }) + } + return req +} + +func makeEdgeXDeviceRequest(devs []*iotv1alpha1.Device) []*requests.AddDeviceRequest { + var req []*requests.AddDeviceRequest + for _, dev := range devs { + req = append(req, &requests.AddDeviceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV2, + }, + }, + Device: toEdgeXDevice(dev), + }) + } + return req +} + +func makeEdgeXDeviceService(dss []*iotv1alpha1.DeviceService) []*requests.AddDeviceServiceRequest { + var req []*requests.AddDeviceServiceRequest + for _, ds := range dss { + req = append(req, &requests.AddDeviceServiceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV2, + }, + }, + Service: toEdgexDeviceService(ds), + }) + } + return req +} + +func toKubeName(edgexName string) string { + return strings.ReplaceAll(strings.ToLower(edgexName), "_", "-") +} diff --git a/pkg/yurtiotdock/clients/errors.go b/pkg/yurtiotdock/clients/errors.go new file mode 100644 index 00000000000..b5d6d5b6a2b --- /dev/null +++ b/pkg/yurtiotdock/clients/errors.go @@ -0,0 +1,29 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package clients + +import "strings" + +type NotFoundError struct{} + +func (e *NotFoundError) Error() string { return "Item not found" } + +func IsNotFoundErr(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "not found") || strings.HasPrefix(err.Error(), "no item found") +} diff --git a/pkg/yurtiotdock/clients/interface.go b/pkg/yurtiotdock/clients/interface.go new file mode 100644 index 00000000000..93ffc3574bd --- /dev/null +++ b/pkg/yurtiotdock/clients/interface.go @@ -0,0 +1,93 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "context" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +// CreateOptions defines additional options when creating an object +// Additional general field definitions can be added +type CreateOptions struct{} + +// DeleteOptions defines additional options when deleting an object +// Additional general field definitions can be added +type DeleteOptions struct{} + +// UpdateOptions defines additional options when updating an object +// Additional general field definitions can be added +type UpdateOptions struct{} + +// GetOptions defines additional options when getting an object +// Additional general field definitions can be added +type GetOptions struct { + // Namespace represents the namespace to list for, or empty for + // non-namespaced objects, or to list across all namespaces. + Namespace string +} + +// ListOptions defines additional options when listing an object +type ListOptions struct { + // A selector to restrict the list of returned objects by their labels. + // Defaults to everything. + // +optional + LabelSelector map[string]string + // A selector to restrict the list of returned objects by their fields. + // Defaults to everything. + // +optional + FieldSelector map[string]string + // Namespace represents the namespace to list for, or empty for + // non-namespaced objects, or to list across all namespaces. + Namespace string +} + +// DeviceInterface defines the interfaces which used to create, delete, update, get and list Device objects on edge-side platform +type DeviceInterface interface { + DevicePropertyInterface + Create(ctx context.Context, device *iotv1alpha1.Device, options CreateOptions) (*iotv1alpha1.Device, error) + Delete(ctx context.Context, name string, options DeleteOptions) error + Update(ctx context.Context, device *iotv1alpha1.Device, options UpdateOptions) (*iotv1alpha1.Device, error) + Get(ctx context.Context, name string, options GetOptions) (*iotv1alpha1.Device, error) + List(ctx context.Context, options ListOptions) ([]iotv1alpha1.Device, error) +} + +// DevicePropertyInterface defines the interfaces which used to get, list and set the actual status value of the device properties +type DevicePropertyInterface interface { + GetPropertyState(ctx context.Context, propertyName string, device *iotv1alpha1.Device, options GetOptions) (*iotv1alpha1.ActualPropertyState, error) + UpdatePropertyState(ctx context.Context, propertyName string, device *iotv1alpha1.Device, options UpdateOptions) error + ListPropertiesState(ctx context.Context, device *iotv1alpha1.Device, options ListOptions) (map[string]iotv1alpha1.DesiredPropertyState, map[string]iotv1alpha1.ActualPropertyState, error) +} + +// DeviceServiceInterface defines the interfaces which used to create, delete, update, get and list DeviceService objects on edge-side platform +type DeviceServiceInterface interface { + Create(ctx context.Context, deviceService *iotv1alpha1.DeviceService, options CreateOptions) (*iotv1alpha1.DeviceService, error) + Delete(ctx context.Context, name string, options DeleteOptions) error + Update(ctx context.Context, deviceService *iotv1alpha1.DeviceService, options UpdateOptions) (*iotv1alpha1.DeviceService, error) + Get(ctx context.Context, name string, options GetOptions) (*iotv1alpha1.DeviceService, error) + List(ctx context.Context, options ListOptions) ([]iotv1alpha1.DeviceService, error) +} + +// DeviceProfileInterface defines the interfaces which used to create, delete, update, get and list DeviceProfile objects on edge-side platform +type DeviceProfileInterface interface { + Create(ctx context.Context, deviceProfile *iotv1alpha1.DeviceProfile, options CreateOptions) (*iotv1alpha1.DeviceProfile, error) + Delete(ctx context.Context, name string, options DeleteOptions) error + Update(ctx context.Context, deviceProfile *iotv1alpha1.DeviceProfile, options UpdateOptions) (*iotv1alpha1.DeviceProfile, error) + Get(ctx context.Context, name string, options GetOptions) (*iotv1alpha1.DeviceProfile, error) + List(ctx context.Context, options ListOptions) ([]iotv1alpha1.DeviceProfile, error) +} diff --git a/pkg/yurtiotdock/controllers/device_controller.go b/pkg/yurtiotdock/controllers/device_controller.go new file mode 100644 index 00000000000..dd61d5a6a96 --- /dev/null +++ b/pkg/yurtiotdock/controllers/device_controller.go @@ -0,0 +1,295 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + util "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +// DeviceReconciler reconciles a Device object +type DeviceReconciler struct { + client.Client + Scheme *runtime.Scheme + deviceCli clients.DeviceInterface + // which nodePool deviceController is deployed in + NodePool string + Namespace string +} + +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=devices,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=devices/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=devices/finalizers,verbs=update + +func (r *DeviceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var d iotv1alpha1.Device + if err := r.Get(ctx, req.NamespacedName, &d); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // If objects doesn't belong to the Edge platform to which the controller is connected, the controller does not handle events for that object + if d.Spec.NodePool != r.NodePool { + return ctrl.Result{}, nil + } + klog.V(3).Infof("Reconciling the Device: %s", d.GetName()) + + deviceStatus := d.Status.DeepCopy() + // Update the conditions for device + defer func() { + if !d.Spec.Managed { + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceManagingReason, "")) + } + + err := r.Status().Update(ctx, &d) + if client.IgnoreNotFound(err) != nil { + if !apierrors.IsConflict(err) { + klog.V(4).ErrorS(err, "update device conditions failed", "DeviceName", d.GetName()) + } + } + }() + + // 1. Handle the device deletion event + if err := r.reconcileDeleteDevice(ctx, &d); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } else if !d.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + if !d.Status.Synced { + // 2. Synchronize OpenYurt device objects to edge platform + if err := r.reconcileCreateDevice(ctx, &d, deviceStatus); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } else { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } else if d.Spec.Managed { + // 3. If the device has been synchronized and is managed by the cloud, reconcile the device properties + if err := r.reconcileUpdateDevice(ctx, &d, deviceStatus); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{RequeueAfter: time.Second * 2}, nil + } + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeviceReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { + r.deviceCli = edgexCli.NewEdgexDeviceClient(opts.CoreMetadataAddr, opts.CoreCommandAddr) + r.NodePool = opts.Nodepool + r.Namespace = opts.Namespace + + return ctrl.NewControllerManagedBy(mgr). + For(&iotv1alpha1.Device{}). + WithEventFilter(genFirstUpdateFilter("device")). + Complete(r) +} + +func (r *DeviceReconciler) reconcileDeleteDevice(ctx context.Context, d *iotv1alpha1.Device) error { + // gets the actual name of the device on the Edge platform from the Label of the device + edgeDeviceName := util.GetEdgeDeviceName(d, EdgeXObjectName) + if d.ObjectMeta.DeletionTimestamp.IsZero() { + if len(d.GetFinalizers()) == 0 { + patchData, _ := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{iotv1alpha1.DeviceFinalizer}, + }, + }) + if err := r.Patch(ctx, d, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + } else { + // delete the device object on the edge platform + err := r.deviceCli.Delete(context.TODO(), edgeDeviceName, clients.DeleteOptions{}) + if err != nil && !clients.IsNotFoundErr(err) { + return err + } + + // delete the device in OpenYurt + patchData, _ := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{}, + }, + }) + if err = r.Patch(ctx, d, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + return nil +} + +func (r *DeviceReconciler) reconcileCreateDevice(ctx context.Context, d *iotv1alpha1.Device, deviceStatus *iotv1alpha1.DeviceStatus) error { + // get the actual name of the device on the Edge platform from the Label of the device + edgeDeviceName := util.GetEdgeDeviceName(d, EdgeXObjectName) + newDeviceStatus := d.Status.DeepCopy() + klog.V(4).Infof("Checking if device already exist on the edge platform: %s", d.GetName()) + // Checking if device already exist on the edge platform + edgeDevice, err := r.deviceCli.Get(context.TODO(), edgeDeviceName, clients.GetOptions{Namespace: r.Namespace}) + if err == nil { + // a. If object exists, the status of the device on OpenYurt is updated + klog.V(4).Infof("Device already exists on edge platform: %s", d.GetName()) + newDeviceStatus.EdgeId = edgeDevice.Status.EdgeId + newDeviceStatus.Synced = true + } else if clients.IsNotFoundErr(err) { + // b. If the object does not exist, a request is sent to the edge platform to create a new device + klog.V(4).Infof("Adding device to the edge platform: %s", d.GetName()) + createdEdgeObj, err := r.deviceCli.Create(context.TODO(), d, clients.CreateOptions{}) + if err != nil { + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceSyncedCondition, corev1.ConditionFalse, iotv1alpha1.DeviceCreateSyncedReason, err.Error())) + return fmt.Errorf("fail to add Device to edge platform: %v", err) + } else { + klog.V(4).Infof("Successfully add Device to edge platform, Name: %s, EdgeId: %s", edgeDeviceName, createdEdgeObj.Status.EdgeId) + newDeviceStatus.EdgeId = createdEdgeObj.Status.EdgeId + newDeviceStatus.Synced = true + } + } else { + klog.V(4).ErrorS(err, "failed to visit the edge platform") + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceSyncedCondition, corev1.ConditionFalse, iotv1alpha1.DeviceVistedCoreMetadataSyncedReason, "")) + return nil + } + d.Status = *newDeviceStatus + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceSyncedCondition, corev1.ConditionTrue, "", "")) + + return r.Status().Update(ctx, d) +} + +func (r *DeviceReconciler) reconcileUpdateDevice(ctx context.Context, d *iotv1alpha1.Device, deviceStatus *iotv1alpha1.DeviceStatus) error { + // the device has been added to the edge platform, check if each device property are in the desired state + newDeviceStatus := d.Status.DeepCopy() + // This list is used to hold the names of properties that failed to reconcile + var failedPropertyNames []string + + // 1. reconciling the AdminState and OperatingState field of device + klog.V(3).Infof("DeviceName: %s, reconciling the AdminState and OperatingState field of device", d.GetName()) + updateDevice := d.DeepCopy() + if d.Spec.AdminState != "" && d.Spec.AdminState != d.Status.AdminState { + newDeviceStatus.AdminState = d.Spec.AdminState + } else { + updateDevice.Spec.AdminState = "" + } + + if d.Spec.OperatingState != "" && d.Spec.OperatingState != d.Status.OperatingState { + newDeviceStatus.OperatingState = d.Spec.OperatingState + } else { + updateDevice.Spec.OperatingState = "" + } + _, err := r.deviceCli.Update(context.TODO(), updateDevice, clients.UpdateOptions{}) + if err != nil { + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceUpdateStateReason, err.Error())) + return err + } + + // 2. reconciling the device properties' value + klog.V(3).Infof("DeviceName: %s, reconciling the device properties", d.GetName()) + // property updates are made only when the device is up and unlocked + if newDeviceStatus.OperatingState == iotv1alpha1.Up && newDeviceStatus.AdminState == iotv1alpha1.UnLocked { + newDeviceStatus, failedPropertyNames = r.reconcileDeviceProperties(d, newDeviceStatus) + } + + d.Status = *newDeviceStatus + + // 3. update the device status on OpenYurt + klog.V(3).Infof("DeviceName: %s, update the device status", d.GetName()) + if err := r.Status().Update(ctx, d); err != nil { + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceUpdateStateReason, err.Error())) + return err + } else if len(failedPropertyNames) != 0 { + err = fmt.Errorf("the following device properties failed to reconcile: %v", failedPropertyNames) + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionFalse, err.Error(), "")) + return nil + } + + util.SetDeviceCondition(deviceStatus, util.NewDeviceCondition(iotv1alpha1.DeviceManagingCondition, corev1.ConditionTrue, "", "")) + return nil +} + +// Update the actual property value of the device on edge platform, +// return the latest status and the names of the property that failed to update +func (r *DeviceReconciler) reconcileDeviceProperties(d *iotv1alpha1.Device, deviceStatus *iotv1alpha1.DeviceStatus) (*iotv1alpha1.DeviceStatus, []string) { + newDeviceStatus := deviceStatus.DeepCopy() + // This list is used to hold the names of properties that failed to reconcile + var failedPropertyNames []string + // 2. reconciling the device properties' value + klog.V(3).Infof("DeviceName: %s, reconciling the value of device properties", d.GetName()) + for _, desiredProperty := range d.Spec.DeviceProperties { + if desiredProperty.DesiredValue == "" { + continue + } + propertyName := desiredProperty.Name + // 1.1. gets the actual property value of the current device from edge platform + klog.V(4).Infof("DeviceName: %s, getting the actual value of property: %s", d.GetName(), propertyName) + actualProperty, err := r.deviceCli.GetPropertyState(context.TODO(), propertyName, d, clients.GetOptions{}) + if err != nil { + if !clients.IsNotFoundErr(err) { + klog.Errorf("DeviceName: %s, failed to get actual property value of %s, err:%v", d.GetName(), propertyName, err) + failedPropertyNames = append(failedPropertyNames, propertyName) + continue + } + klog.Errorf("DeviceName: %s, property read command not found", d.GetName()) + } else { + klog.V(4).Infof("DeviceName: %s, got the actual property state, {Name: %s, GetURL: %s, ActualValue: %s}", + d.GetName(), propertyName, actualProperty.GetURL, actualProperty.ActualValue) + } + + if newDeviceStatus.DeviceProperties == nil { + newDeviceStatus.DeviceProperties = map[string]iotv1alpha1.ActualPropertyState{} + } else { + newDeviceStatus.DeviceProperties[propertyName] = *actualProperty + } + + // 1.2. set the device attribute in the edge platform to the expected value + if actualProperty == nil || desiredProperty.DesiredValue != actualProperty.ActualValue { + klog.V(4).Infof("DeviceName: %s, the desired value and the actual value are different, desired: %s, actual: %s", + d.GetName(), desiredProperty.DesiredValue, actualProperty.ActualValue) + if err := r.deviceCli.UpdatePropertyState(context.TODO(), propertyName, d, clients.UpdateOptions{}); err != nil { + klog.ErrorS(err, "failed to update property", "DeviceName", d.GetName(), "propertyName", propertyName) + failedPropertyNames = append(failedPropertyNames, propertyName) + continue + } + + klog.V(4).Infof("DeviceName: %s, successfully set the property %s to desired value", d.GetName(), propertyName) + newActualProperty := iotv1alpha1.ActualPropertyState{ + Name: propertyName, + GetURL: actualProperty.GetURL, + ActualValue: desiredProperty.DesiredValue, + } + newDeviceStatus.DeviceProperties[propertyName] = newActualProperty + } + } + return newDeviceStatus, failedPropertyNames +} diff --git a/pkg/yurtiotdock/controllers/device_syncer.go b/pkg/yurtiotdock/controllers/device_syncer.go new file mode 100644 index 00000000000..501ba6101dc --- /dev/null +++ b/pkg/yurtiotdock/controllers/device_syncer.go @@ -0,0 +1,237 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlmgr "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + edgeCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + efCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +type DeviceSyncer struct { + // kubernetes client + client.Client + // which nodePool deviceController is deployed in + NodePool string + // edge platform's client + deviceCli edgeCli.DeviceInterface + // syncing period in seconds + syncPeriod time.Duration + Namespace string +} + +// NewDeviceSyncer initialize a New DeviceSyncer +func NewDeviceSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceSyncer, error) { + return DeviceSyncer{ + syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, + deviceCli: efCli.NewEdgexDeviceClient(opts.CoreMetadataAddr, opts.CoreCommandAddr), + Client: client, + NodePool: opts.Nodepool, + Namespace: opts.Namespace, + }, nil +} + +// NewDeviceSyncerRunnable initialize a controller-runtime manager runnable +func (ds *DeviceSyncer) NewDeviceSyncerRunnable() ctrlmgr.RunnableFunc { + return func(ctx context.Context) error { + ds.Run(ctx.Done()) + return nil + } +} + +func (ds *DeviceSyncer) Run(stop <-chan struct{}) { + klog.V(1).Info("[Device] Starting the syncer...") + go func() { + for { + <-time.After(ds.syncPeriod) + klog.V(2).Info("[Device] Start a round of synchronization.") + // 1. get device on edge platform and OpenYurt + edgeDevices, kubeDevices, err := ds.getAllDevices() + if err != nil { + klog.V(3).ErrorS(err, "fail to list the devices") + continue + } + + // 2. find the device that need to be synchronized + redundantEdgeDevices, redundantKubeDevices, syncedDevices := ds.findDiffDevice(edgeDevices, kubeDevices) + klog.V(2).Infof("[Device] The number of objects waiting for synchronization { %s:%d, %s:%d, %s:%d }", + "Edge device should be added to OpenYurt", len(redundantEdgeDevices), + "OpenYurt device that should be deleted", len(redundantKubeDevices), + "Devices that should be synchronized", len(syncedDevices)) + + // 3. create device on OpenYurt which are exists in edge platform but not in OpenYurt + if err := ds.syncEdgeToKube(redundantEdgeDevices); err != nil { + klog.V(3).ErrorS(err, "fail to create devices on OpenYurt") + } + + // 4. delete redundant device on OpenYurt + if err := ds.deleteDevices(redundantKubeDevices); err != nil { + klog.V(3).ErrorS(err, "fail to delete redundant devices on OpenYurt") + } + + // 5. update device status on OpenYurt + if err := ds.updateDevices(syncedDevices); err != nil { + klog.V(3).ErrorS(err, "fail to update devices status") + } + klog.V(2).Info("[Device] One round of synchronization is complete") + } + }() + + <-stop + klog.V(1).Info("[Device] Stopping the syncer") +} + +// Get the existing Device on the Edge platform, as well as OpenYurt existing Device +// edgeDevice:map[actualName]device +// kubeDevice:map[actualName]device +func (ds *DeviceSyncer) getAllDevices() (map[string]iotv1alpha1.Device, map[string]iotv1alpha1.Device, error) { + edgeDevice := map[string]iotv1alpha1.Device{} + kubeDevice := map[string]iotv1alpha1.Device{} + // 1. list devices on edge platform + eDevs, err := ds.deviceCli.List(context.TODO(), edgeCli.ListOptions{Namespace: ds.Namespace}) + if err != nil { + klog.V(4).ErrorS(err, "fail to list the devices object on the Edge Platform") + return edgeDevice, kubeDevice, err + } + // 2. list devices on OpenYurt (filter objects belonging to edgeServer) + var kDevs iotv1alpha1.DeviceList + listOptions := client.MatchingFields{util.IndexerPathForNodepool: ds.NodePool} + if err = ds.List(context.TODO(), &kDevs, listOptions, client.InNamespace(ds.Namespace)); err != nil { + klog.V(4).ErrorS(err, "fail to list the devices object on the OpenYurt") + return edgeDevice, kubeDevice, err + } + for i := range eDevs { + deviceName := util.GetEdgeDeviceName(&eDevs[i], EdgeXObjectName) + edgeDevice[deviceName] = eDevs[i] + } + + for i := range kDevs.Items { + deviceName := util.GetEdgeDeviceName(&kDevs.Items[i], EdgeXObjectName) + kubeDevice[deviceName] = kDevs.Items[i] + } + return edgeDevice, kubeDevice, nil +} + +// Get the list of devices that need to be added, deleted and updated +func (ds *DeviceSyncer) findDiffDevice( + edgeDevices map[string]iotv1alpha1.Device, kubeDevices map[string]iotv1alpha1.Device) ( + redundantEdgeDevices map[string]*iotv1alpha1.Device, redundantKubeDevices map[string]*iotv1alpha1.Device, syncedDevices map[string]*iotv1alpha1.Device) { + + redundantEdgeDevices = map[string]*iotv1alpha1.Device{} + redundantKubeDevices = map[string]*iotv1alpha1.Device{} + syncedDevices = map[string]*iotv1alpha1.Device{} + + for i := range edgeDevices { + ed := edgeDevices[i] + edName := util.GetEdgeDeviceName(&ed, EdgeXObjectName) + if _, exists := kubeDevices[edName]; !exists { + klog.V(5).Infof("found redundant edge device %s", edName) + redundantEdgeDevices[edName] = ds.completeCreateContent(&ed) + } else { + klog.V(5).Infof("found device %s to be synced", edName) + kd := kubeDevices[edName] + syncedDevices[edName] = ds.completeUpdateContent(&kd, &ed) + } + } + + for i := range kubeDevices { + kd := kubeDevices[i] + if !kd.Status.Synced { + continue + } + kdName := util.GetEdgeDeviceName(&kd, EdgeXObjectName) + if _, exists := edgeDevices[kdName]; !exists { + redundantKubeDevices[kdName] = &kd + } + } + return +} + +// syncEdgeToKube creates device on OpenYurt which are exists in edge platform but not in OpenYurt +func (ds *DeviceSyncer) syncEdgeToKube(edgeDevs map[string]*iotv1alpha1.Device) error { + for _, ed := range edgeDevs { + if err := ds.Client.Create(context.TODO(), ed); err != nil { + if apierrors.IsAlreadyExists(err) { + continue + } + klog.V(5).ErrorS(err, "fail to create device on OpenYurt", "DeviceName", strings.ToLower(ed.Name)) + return err + } + } + return nil +} + +// deleteDevices deletes redundant device on OpenYurt +func (ds *DeviceSyncer) deleteDevices(redundantKubeDevices map[string]*iotv1alpha1.Device) error { + for _, kd := range redundantKubeDevices { + if err := ds.Client.Delete(context.TODO(), kd); err != nil { + klog.V(5).ErrorS(err, "fail to delete the device on OpenYurt", + "DeviceName", kd.Name) + return err + } + } + return nil +} + +// updateDevicesStatus updates device status on OpenYurt +func (ds *DeviceSyncer) updateDevices(syncedDevices map[string]*iotv1alpha1.Device) error { + for n := range syncedDevices { + if err := ds.Client.Status().Update(context.TODO(), syncedDevices[n]); err != nil { + if apierrors.IsConflict(err) { + klog.V(5).InfoS("update Conflicts", "Device", syncedDevices[n].Name) + continue + } + return err + } + } + return nil +} + +// completeCreateContent completes the content of the device which will be created on OpenYurt +func (ds *DeviceSyncer) completeCreateContent(edgeDevice *iotv1alpha1.Device) *iotv1alpha1.Device { + createDevice := edgeDevice.DeepCopy() + createDevice.Spec.NodePool = ds.NodePool + createDevice.Name = strings.Join([]string{ds.NodePool, createDevice.Name}, "-") + createDevice.Namespace = ds.Namespace + createDevice.Spec.Managed = false + + return createDevice +} + +// completeUpdateContent completes the content of the device which will be updated on OpenYurt +func (ds *DeviceSyncer) completeUpdateContent(kubeDevice *iotv1alpha1.Device, edgeDevice *iotv1alpha1.Device) *iotv1alpha1.Device { + updatedDevice := kubeDevice.DeepCopy() + _, aps, _ := ds.deviceCli.ListPropertiesState(context.TODO(), updatedDevice, edgeCli.ListOptions{}) + // update device status + updatedDevice.Status.LastConnected = edgeDevice.Status.LastConnected + updatedDevice.Status.LastReported = edgeDevice.Status.LastReported + updatedDevice.Status.AdminState = edgeDevice.Status.AdminState + updatedDevice.Status.OperatingState = edgeDevice.Status.OperatingState + updatedDevice.Status.DeviceProperties = aps + return updatedDevice +} diff --git a/pkg/yurtiotdock/controllers/deviceprofile_controller.go b/pkg/yurtiotdock/controllers/deviceprofile_controller.go new file mode 100644 index 00000000000..d174cc2ebb6 --- /dev/null +++ b/pkg/yurtiotdock/controllers/deviceprofile_controller.go @@ -0,0 +1,165 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexclis "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +// DeviceProfileReconciler reconciles a DeviceProfile object +type DeviceProfileReconciler struct { + client.Client + Scheme *runtime.Scheme + edgeClient clients.DeviceProfileInterface + NodePool string + Namespace string +} + +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceprofiles,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceprofiles/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceprofiles/finalizers,verbs=update + +// Reconcile make changes to a deviceprofile object in EdgeX based on it in Kubernetes +func (r *DeviceProfileReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var dp iotv1alpha1.DeviceProfile + if err := r.Get(ctx, req.NamespacedName, &dp); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + if dp.Spec.NodePool != r.NodePool { + return ctrl.Result{}, nil + } + klog.V(3).Infof("Reconciling the DeviceProfile: %s", dp.GetName()) + + // gets the actual name of deviceProfile on the edge platform from the Label of the deviceProfile + dpActualName := util.GetEdgeDeviceProfileName(&dp, EdgeXObjectName) + + // 1. Handle the deviceProfile deletion event + if err := r.reconcileDeleteDeviceProfile(ctx, &dp, dpActualName); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } else if !dp.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + if !dp.Status.Synced { + // 2. Synchronize OpenYurt deviceProfile to edge platform + if err := r.reconcileCreateDeviceProfile(ctx, &dp, dpActualName); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } else { + return ctrl.Result{}, err + } + } + } + // 3. Handle the deviceProfile update event + // TODO + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeviceProfileReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { + r.edgeClient = edgexclis.NewEdgexDeviceProfile(opts.CoreMetadataAddr) + r.NodePool = opts.Nodepool + r.Namespace = opts.Namespace + + return ctrl.NewControllerManagedBy(mgr). + For(&iotv1alpha1.DeviceProfile{}). + WithEventFilter(genFirstUpdateFilter("deviceprofile")). + Complete(r) +} + +func (r *DeviceProfileReconciler) reconcileDeleteDeviceProfile(ctx context.Context, dp *iotv1alpha1.DeviceProfile, actualName string) error { + if dp.ObjectMeta.DeletionTimestamp.IsZero() { + if len(dp.GetFinalizers()) == 0 { + patchString := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{iotv1alpha1.DeviceProfileFinalizer}, + }, + } + if patchData, err := json.Marshal(patchString); err != nil { + return err + } else { + if err = r.Patch(ctx, dp, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + } + } else { + patchString := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{}, + }, + } + // delete the deviceProfile in OpenYurt + if patchData, err := json.Marshal(patchString); err != nil { + return err + } else { + if err = r.Patch(ctx, dp, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + + // delete the deviceProfile object on edge platform + err := r.edgeClient.Delete(context.TODO(), actualName, clients.DeleteOptions{}) + if err != nil && !clients.IsNotFoundErr(err) { + return err + } + } + return nil +} + +func (r *DeviceProfileReconciler) reconcileCreateDeviceProfile(ctx context.Context, dp *iotv1alpha1.DeviceProfile, actualName string) error { + klog.V(4).Infof("Checking if deviceProfile already exist on the edge platform: %s", dp.GetName()) + if edgeDp, err := r.edgeClient.Get(context.TODO(), actualName, clients.GetOptions{Namespace: r.Namespace}); err != nil { + if !clients.IsNotFoundErr(err) { + klog.V(4).ErrorS(err, "fail to visit the edge platform") + return nil + } + } else { + // a. If object exists, the status of the deviceProfile on OpenYurt is updated + klog.V(4).Info("DeviceProfile already exists on edge platform") + dp.Status.Synced = true + dp.Status.EdgeId = edgeDp.Status.EdgeId + return r.Status().Update(ctx, dp) + } + + // b. If object does not exist, a request is sent to the edge platform to create a new deviceProfile + createDp, err := r.edgeClient.Create(context.Background(), dp, clients.CreateOptions{}) + if err != nil { + klog.V(4).ErrorS(err, "failed to create deviceProfile on edge platform") + return fmt.Errorf("failed to add deviceProfile to edge platform: %v", err) + } + klog.V(3).Infof("Successfully add DeviceProfile to edge platform, Name: %s, EdgeId: %s", createDp.GetName(), createDp.Status.EdgeId) + dp.Status.EdgeId = createDp.Status.EdgeId + dp.Status.Synced = true + return r.Status().Update(ctx, dp) +} diff --git a/pkg/yurtiotdock/controllers/deviceprofile_syncer.go b/pkg/yurtiotdock/controllers/deviceprofile_syncer.go new file mode 100644 index 00000000000..72d67729b47 --- /dev/null +++ b/pkg/yurtiotdock/controllers/deviceprofile_syncer.go @@ -0,0 +1,214 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlmgr "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + devcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexclis "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +type DeviceProfileSyncer struct { + // syncing period in seconds + syncPeriod time.Duration + // edge platform client + edgeClient devcli.DeviceProfileInterface + // Kubernetes client + client.Client + NodePool string + Namespace string +} + +// NewDeviceProfileSyncer initialize a New DeviceProfileSyncer +func NewDeviceProfileSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceProfileSyncer, error) { + return DeviceProfileSyncer{ + syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, + edgeClient: edgexclis.NewEdgexDeviceProfile(opts.CoreMetadataAddr), + Client: client, + NodePool: opts.Nodepool, + Namespace: opts.Namespace, + }, nil +} + +// NewDeviceProfileSyncerRunnable initialize a controller-runtime manager runnable +func (dps *DeviceProfileSyncer) NewDeviceProfileSyncerRunnable() ctrlmgr.RunnableFunc { + return func(ctx context.Context) error { + dps.Run(ctx.Done()) + return nil + } +} + +func (dps *DeviceProfileSyncer) Run(stop <-chan struct{}) { + klog.V(1).Info("[DeviceProfile] Starting the syncer...") + go func() { + for { + <-time.After(dps.syncPeriod) + klog.V(2).Info("[DeviceProfile] Start a round of synchronization.") + + // 1. get deviceProfiles on edge platform and OpenYurt + edgeDeviceProfiles, kubeDeviceProfiles, err := dps.getAllDeviceProfiles() + if err != nil { + klog.V(3).ErrorS(err, "fail to list the deviceProfiles") + continue + } + + // 2. find the deviceProfiles that need to be synchronized + redundantEdgeDeviceProfiles, redundantKubeDeviceProfiles, syncedDeviceProfiles := + dps.findDiffDeviceProfiles(edgeDeviceProfiles, kubeDeviceProfiles) + klog.V(2).Infof("[DeviceProfile] The number of objects waiting for synchronization { %s:%d, %s:%d, %s:%d }", + "Edge deviceProfiles should be added to OpenYurt", len(redundantEdgeDeviceProfiles), + "OpenYurt deviceProfiles that should be deleted", len(redundantKubeDeviceProfiles), + "DeviceProfiles that should be synchronized", len(syncedDeviceProfiles)) + + // 3. create deviceProfiles on OpenYurt which are exists in edge platform but not in OpenYurt + if err := dps.syncEdgeToKube(redundantEdgeDeviceProfiles); err != nil { + klog.V(3).ErrorS(err, "fail to create deviceProfiles on OpenYurt") + } + + // 4. delete redundant deviceProfiles on OpenYurt + if err := dps.deleteDeviceProfiles(redundantKubeDeviceProfiles); err != nil { + klog.V(3).ErrorS(err, "fail to delete redundant deviceProfiles on OpenYurt") + } + + // 5. update deviceProfiles on OpenYurt + // TODO + } + }() + + <-stop + klog.V(1).Info("[DeviceProfile] Stopping the syncer") +} + +// Get the existing DeviceProfile on the Edge platform, as well as OpenYurt existing DeviceProfile +// edgeDeviceProfiles:map[actualName]DeviceProfile +// kubeDeviceProfiles:map[actualName]DeviceProfile +func (dps *DeviceProfileSyncer) getAllDeviceProfiles() ( + map[string]iotv1alpha1.DeviceProfile, map[string]iotv1alpha1.DeviceProfile, error) { + + edgeDeviceProfiles := map[string]iotv1alpha1.DeviceProfile{} + kubeDeviceProfiles := map[string]iotv1alpha1.DeviceProfile{} + + // 1. list deviceProfiles on edge platform + eDps, err := dps.edgeClient.List(context.TODO(), devcli.ListOptions{Namespace: dps.Namespace}) + if err != nil { + klog.V(4).ErrorS(err, "fail to list the deviceProfiles on the edge platform") + return edgeDeviceProfiles, kubeDeviceProfiles, err + } + // 2. list deviceProfiles on OpenYurt (filter objects belonging to edgeServer) + var kDps iotv1alpha1.DeviceProfileList + listOptions := client.MatchingFields{util.IndexerPathForNodepool: dps.NodePool} + if err = dps.List(context.TODO(), &kDps, listOptions, client.InNamespace(dps.Namespace)); err != nil { + klog.V(4).ErrorS(err, "fail to list the deviceProfiles on the Kubernetes") + return edgeDeviceProfiles, kubeDeviceProfiles, err + } + for i := range eDps { + deviceProfilesName := util.GetEdgeDeviceProfileName(&eDps[i], EdgeXObjectName) + edgeDeviceProfiles[deviceProfilesName] = eDps[i] + } + + for i := range kDps.Items { + deviceProfilesName := util.GetEdgeDeviceProfileName(&kDps.Items[i], EdgeXObjectName) + kubeDeviceProfiles[deviceProfilesName] = kDps.Items[i] + } + return edgeDeviceProfiles, kubeDeviceProfiles, nil +} + +// Get the list of deviceProfiles that need to be added, deleted and updated +func (dps *DeviceProfileSyncer) findDiffDeviceProfiles( + edgeDeviceProfiles map[string]iotv1alpha1.DeviceProfile, kubeDeviceProfiles map[string]iotv1alpha1.DeviceProfile) ( + redundantEdgeDeviceProfiles map[string]*iotv1alpha1.DeviceProfile, redundantKubeDeviceProfiles map[string]*iotv1alpha1.DeviceProfile, syncedDeviceProfiles map[string]*iotv1alpha1.DeviceProfile) { + + redundantEdgeDeviceProfiles = map[string]*iotv1alpha1.DeviceProfile{} + redundantKubeDeviceProfiles = map[string]*iotv1alpha1.DeviceProfile{} + syncedDeviceProfiles = map[string]*iotv1alpha1.DeviceProfile{} + + for i := range edgeDeviceProfiles { + edp := edgeDeviceProfiles[i] + edpName := util.GetEdgeDeviceProfileName(&edp, EdgeXObjectName) + if _, exists := kubeDeviceProfiles[edpName]; !exists { + redundantEdgeDeviceProfiles[edpName] = dps.completeCreateContent(&edp) + } else { + kdp := kubeDeviceProfiles[edpName] + syncedDeviceProfiles[edpName] = dps.completeUpdateContent(&kdp, &edp) + } + } + + for i := range kubeDeviceProfiles { + kdp := kubeDeviceProfiles[i] + if !kdp.Status.Synced { + continue + } + kdpName := util.GetEdgeDeviceProfileName(&kdp, EdgeXObjectName) + if _, exists := edgeDeviceProfiles[kdpName]; !exists { + redundantKubeDeviceProfiles[kdpName] = &kdp + } + } + return +} + +// completeCreateContent completes the content of the deviceProfile which will be created on OpenYurt +func (dps *DeviceProfileSyncer) completeCreateContent(edgeDps *iotv1alpha1.DeviceProfile) *iotv1alpha1.DeviceProfile { + createDeviceProfile := edgeDps.DeepCopy() + createDeviceProfile.Namespace = dps.Namespace + createDeviceProfile.Name = strings.Join([]string{dps.NodePool, createDeviceProfile.Name}, "-") + createDeviceProfile.Spec.NodePool = dps.NodePool + return createDeviceProfile +} + +// completeUpdateContent completes the content of the deviceProfile which will be updated on OpenYurt +// TODO +func (dps *DeviceProfileSyncer) completeUpdateContent(kubeDps *iotv1alpha1.DeviceProfile, edgeDS *iotv1alpha1.DeviceProfile) *iotv1alpha1.DeviceProfile { + return kubeDps +} + +// syncEdgeToKube creates deviceProfiles on OpenYurt which are exists in edge platform but not in OpenYurt +func (dps *DeviceProfileSyncer) syncEdgeToKube(edgeDps map[string]*iotv1alpha1.DeviceProfile) error { + for _, edp := range edgeDps { + if err := dps.Client.Create(context.TODO(), edp); err != nil { + if apierrors.IsAlreadyExists(err) { + klog.V(5).Infof("DeviceProfile already exist on Kubernetes: %s", strings.ToLower(edp.Name)) + continue + } + klog.Infof("created deviceProfile failed: %s", strings.ToLower(edp.Name)) + return err + } + } + return nil +} + +// deleteDeviceProfiles deletes redundant deviceProfiles on OpenYurt +func (dps *DeviceProfileSyncer) deleteDeviceProfiles(redundantKubeDeviceProfiles map[string]*iotv1alpha1.DeviceProfile) error { + for _, kdp := range redundantKubeDeviceProfiles { + if err := dps.Client.Delete(context.TODO(), kdp); err != nil { + klog.V(5).ErrorS(err, "fail to delete the DeviceProfile on Kubernetes: %s ", + "DeviceProfile", kdp.Name) + return err + } + } + return nil +} diff --git a/pkg/yurtiotdock/controllers/deviceservice_controller.go b/pkg/yurtiotdock/controllers/deviceservice_controller.go new file mode 100644 index 00000000000..2822558b5f0 --- /dev/null +++ b/pkg/yurtiotdock/controllers/deviceservice_controller.go @@ -0,0 +1,220 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + util "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +// DeviceServiceReconciler reconciles a DeviceService object +type DeviceServiceReconciler struct { + client.Client + Scheme *runtime.Scheme + deviceServiceCli clients.DeviceServiceInterface + NodePool string + Namespace string +} + +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceservices,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceservices/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=iot.openyurt.io,resources=deviceservices/finalizers,verbs=update + +func (r *DeviceServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var ds iotv1alpha1.DeviceService + if err := r.Get(ctx, req.NamespacedName, &ds); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // If objects doesn't belong to the edge platform to which the controller is connected, the controller does not handle events for that object + if ds.Spec.NodePool != r.NodePool { + return ctrl.Result{}, nil + } + klog.V(3).Infof("Reconciling the DeviceService: %s", ds.GetName()) + + deviceServiceStatus := ds.Status.DeepCopy() + // Update deviceService conditions + defer func() { + if !ds.Spec.Managed { + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceServiceManagingReason, "")) + } + + err := r.Status().Update(ctx, &ds) + if client.IgnoreNotFound(err) != nil { + if !apierrors.IsConflict(err) { + klog.V(4).ErrorS(err, "update deviceService conditions failed", "deviceService", ds.GetName()) + } + } + }() + + // 1. Handle the deviceService deletion event + if err := r.reconcileDeleteDeviceService(ctx, &ds); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } else if !ds.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + if !ds.Status.Synced { + // 2. Synchronize OpenYurt deviceService to edge platform + if err := r.reconcileCreateDeviceService(ctx, &ds, deviceServiceStatus); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } else { + return ctrl.Result{}, err + } + } + } else if ds.Spec.Managed { + // 3. If the deviceService has been synchronized and is managed by the cloud, reconcile the deviceService fields + if err := r.reconcileUpdateDeviceService(ctx, &ds, deviceServiceStatus); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } else { + return ctrl.Result{}, err + } + } + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeviceServiceReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { + r.deviceServiceCli = edgexCli.NewEdgexDeviceServiceClient(opts.CoreMetadataAddr) + r.NodePool = opts.Nodepool + r.Namespace = opts.Namespace + + return ctrl.NewControllerManagedBy(mgr). + For(&iotv1alpha1.DeviceService{}). + Complete(r) +} + +func (r *DeviceServiceReconciler) reconcileDeleteDeviceService(ctx context.Context, ds *iotv1alpha1.DeviceService) error { + // gets the actual name of deviceService on the edge platform from the Label of the device + edgeDeviceServiceName := util.GetEdgeDeviceServiceName(ds, EdgeXObjectName) + if ds.ObjectMeta.DeletionTimestamp.IsZero() { + if len(ds.GetFinalizers()) == 0 { + patchString := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{iotv1alpha1.DeviceServiceFinalizer}, + }, + } + if patchData, err := json.Marshal(patchString); err != nil { + return err + } else { + if err = r.Patch(ctx, ds, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + } + } else { + patchString := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{}, + }, + } + // delete the deviceService in OpenYurt + if patchData, err := json.Marshal(patchString); err != nil { + return err + } else { + if err = r.Patch(ctx, ds, client.RawPatch(types.MergePatchType, patchData)); err != nil { + return err + } + } + + // delete the deviceService object on edge platform + err := r.deviceServiceCli.Delete(context.TODO(), edgeDeviceServiceName, clients.DeleteOptions{}) + if err != nil && !clients.IsNotFoundErr(err) { + return err + } + } + return nil +} + +func (r *DeviceServiceReconciler) reconcileCreateDeviceService(ctx context.Context, ds *iotv1alpha1.DeviceService, deviceServiceStatus *iotv1alpha1.DeviceServiceStatus) error { + // get the actual name of deviceService on the Edge platform from the Label of the device + edgeDeviceServiceName := util.GetEdgeDeviceServiceName(ds, EdgeXObjectName) + klog.V(4).Infof("Checking if deviceService already exist on the edge platform: %s", ds.GetName()) + // Checking if deviceService already exist on the edge platform + if edgeDs, err := r.deviceServiceCli.Get(context.TODO(), edgeDeviceServiceName, clients.GetOptions{Namespace: r.Namespace}); err != nil { + if !clients.IsNotFoundErr(err) { + klog.V(4).ErrorS(err, "fail to visit the edge platform") + return nil + } else { + createdDs, err := r.deviceServiceCli.Create(context.TODO(), ds, clients.CreateOptions{}) + if err != nil { + klog.V(4).ErrorS(err, "failed to create deviceService on edge platform") + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceSyncedCondition, corev1.ConditionFalse, iotv1alpha1.DeviceServiceCreateSyncedReason, err.Error())) + return fmt.Errorf("fail to create DeviceService to edge platform: %v", err) + } + + klog.V(4).Infof("Successfully add DeviceService to Edge Platform, Name: %s, EdgeId: %s", ds.GetName(), createdDs.Status.EdgeId) + ds.Status.EdgeId = createdDs.Status.EdgeId + ds.Status.Synced = true + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceSyncedCondition, corev1.ConditionTrue, "", "")) + return r.Status().Update(ctx, ds) + } + } else { + // a. If object exists, the status of the device on OpenYurt is updated + klog.V(4).Infof("DeviceServiceName: %s, obj already exists on edge platform", ds.GetName()) + ds.Status.Synced = true + ds.Status.EdgeId = edgeDs.Status.EdgeId + return r.Status().Update(ctx, ds) + } +} + +func (r *DeviceServiceReconciler) reconcileUpdateDeviceService(ctx context.Context, ds *iotv1alpha1.DeviceService, deviceServiceStatus *iotv1alpha1.DeviceServiceStatus) error { + // 1. reconciling the AdminState field of deviceService + newDeviceServiceStatus := ds.Status.DeepCopy() + updateDeviceService := ds.DeepCopy() + + if ds.Spec.AdminState != "" && ds.Spec.AdminState != ds.Status.AdminState { + newDeviceServiceStatus.AdminState = ds.Spec.AdminState + } else { + updateDeviceService.Spec.AdminState = "" + } + + _, err := r.deviceServiceCli.Update(context.TODO(), updateDeviceService, clients.UpdateOptions{}) + if err != nil { + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceServiceUpdateStatusSyncedReason, err.Error())) + + return err + } + + // 2. update the device status on OpenYurt + ds.Status = *newDeviceServiceStatus + if err = r.Status().Update(ctx, ds); err != nil { + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionFalse, iotv1alpha1.DeviceServiceUpdateStatusSyncedReason, err.Error())) + + return err + } + util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionTrue, "", "")) + return nil +} diff --git a/pkg/yurtiotdock/controllers/deviceservice_syncer.go b/pkg/yurtiotdock/controllers/deviceservice_syncer.go new file mode 100644 index 00000000000..78949c55069 --- /dev/null +++ b/pkg/yurtiotdock/controllers/deviceservice_syncer.go @@ -0,0 +1,237 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package controllers + +import ( + "context" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlmgr "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + iotcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +type DeviceServiceSyncer struct { + // Kubernetes client + client.Client + // syncing period in seconds + syncPeriod time.Duration + deviceServiceCli iotcli.DeviceServiceInterface + NodePool string + Namespace string +} + +func NewDeviceServiceSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceServiceSyncer, error) { + return DeviceServiceSyncer{ + syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, + deviceServiceCli: edgexCli.NewEdgexDeviceServiceClient(opts.CoreMetadataAddr), + Client: client, + NodePool: opts.Nodepool, + Namespace: opts.Namespace, + }, nil +} + +func (ds *DeviceServiceSyncer) NewDeviceServiceSyncerRunnable() ctrlmgr.RunnableFunc { + return func(ctx context.Context) error { + ds.Run(ctx.Done()) + return nil + } +} + +func (ds *DeviceServiceSyncer) Run(stop <-chan struct{}) { + klog.V(1).Info("[DeviceService] Starting the syncer...") + go func() { + for { + <-time.After(ds.syncPeriod) + klog.V(2).Info("[DeviceService] Start a round of synchronization.") + // 1. get deviceServices on edge platform and OpenYurt + edgeDeviceServices, kubeDeviceServices, err := ds.getAllDeviceServices() + if err != nil { + klog.V(3).ErrorS(err, "fail to list the deviceServices") + continue + } + + // 2. find the deviceServices that need to be synchronized + redundantEdgeDeviceServices, redundantKubeDeviceServices, syncedDeviceServices := + ds.findDiffDeviceServices(edgeDeviceServices, kubeDeviceServices) + klog.V(2).Infof("[DeviceService] The number of objects waiting for synchronization { %s:%d, %s:%d, %s:%d }", + "Edge deviceServices should be added to OpenYurt", len(redundantEdgeDeviceServices), + "OpenYurt deviceServices that should be deleted", len(redundantKubeDeviceServices), + "DeviceServices that should be synchronized", len(syncedDeviceServices)) + + // 3. create deviceServices on OpenYurt which are exists in edge platform but not in OpenYurt + if err := ds.syncEdgeToKube(redundantEdgeDeviceServices); err != nil { + klog.V(3).ErrorS(err, "fail to create deviceServices on OpenYurt") + } + + // 4. delete redundant deviceServices on OpenYurt + if err := ds.deleteDeviceServices(redundantKubeDeviceServices); err != nil { + klog.V(3).ErrorS(err, "fail to delete redundant deviceServices on OpenYurt") + } + + // 5. update deviceService status on OpenYurt + if err := ds.updateDeviceServices(syncedDeviceServices); err != nil { + klog.V(3).ErrorS(err, "fail to update deviceServices") + } + klog.V(2).Info("[DeviceService] One round of synchronization is complete") + } + }() + + <-stop + klog.V(1).Info("[DeviceService] Stopping the syncer") +} + +// Get the existing DeviceService on the Edge platform, as well as OpenYurt existing DeviceService +// edgeDeviceServices:map[actualName]DeviceService +// kubeDeviceServices:map[actualName]DeviceService +func (ds *DeviceServiceSyncer) getAllDeviceServices() ( + map[string]iotv1alpha1.DeviceService, map[string]iotv1alpha1.DeviceService, error) { + + edgeDeviceServices := map[string]iotv1alpha1.DeviceService{} + kubeDeviceServices := map[string]iotv1alpha1.DeviceService{} + + // 1. list deviceServices on edge platform + eDevSs, err := ds.deviceServiceCli.List(context.TODO(), iotcli.ListOptions{Namespace: ds.Namespace}) + if err != nil { + klog.V(4).ErrorS(err, "fail to list the deviceServices object on the edge platform") + return edgeDeviceServices, kubeDeviceServices, err + } + // 2. list deviceServices on OpenYurt (filter objects belonging to edgeServer) + var kDevSs iotv1alpha1.DeviceServiceList + listOptions := client.MatchingFields{util.IndexerPathForNodepool: ds.NodePool} + if err = ds.List(context.TODO(), &kDevSs, listOptions, client.InNamespace(ds.Namespace)); err != nil { + klog.V(4).ErrorS(err, "fail to list the deviceServices object on the Kubernetes") + return edgeDeviceServices, kubeDeviceServices, err + } + for i := range eDevSs { + deviceServicesName := util.GetEdgeDeviceServiceName(&eDevSs[i], EdgeXObjectName) + edgeDeviceServices[deviceServicesName] = eDevSs[i] + } + + for i := range kDevSs.Items { + deviceServicesName := util.GetEdgeDeviceServiceName(&kDevSs.Items[i], EdgeXObjectName) + kubeDeviceServices[deviceServicesName] = kDevSs.Items[i] + } + return edgeDeviceServices, kubeDeviceServices, nil +} + +// Get the list of deviceServices that need to be added, deleted and updated +func (ds *DeviceServiceSyncer) findDiffDeviceServices( + edgeDeviceService map[string]iotv1alpha1.DeviceService, kubeDeviceService map[string]iotv1alpha1.DeviceService) ( + redundantEdgeDeviceServices map[string]*iotv1alpha1.DeviceService, redundantKubeDeviceServices map[string]*iotv1alpha1.DeviceService, syncedDeviceServices map[string]*iotv1alpha1.DeviceService) { + + redundantEdgeDeviceServices = map[string]*iotv1alpha1.DeviceService{} + redundantKubeDeviceServices = map[string]*iotv1alpha1.DeviceService{} + syncedDeviceServices = map[string]*iotv1alpha1.DeviceService{} + + for i := range edgeDeviceService { + eds := edgeDeviceService[i] + edName := util.GetEdgeDeviceServiceName(&eds, EdgeXObjectName) + if _, exists := kubeDeviceService[edName]; !exists { + redundantEdgeDeviceServices[edName] = ds.completeCreateContent(&eds) + } else { + kd := kubeDeviceService[edName] + syncedDeviceServices[edName] = ds.completeUpdateContent(&kd, &eds) + } + } + + for i := range kubeDeviceService { + kds := kubeDeviceService[i] + if !kds.Status.Synced { + continue + } + kdName := util.GetEdgeDeviceServiceName(&kds, EdgeXObjectName) + if _, exists := edgeDeviceService[kdName]; !exists { + redundantKubeDeviceServices[kdName] = &kds + } + } + return +} + +// syncEdgeToKube creates deviceServices on OpenYurt which are exists in edge platform but not in OpenYurt +func (ds *DeviceServiceSyncer) syncEdgeToKube(edgeDevs map[string]*iotv1alpha1.DeviceService) error { + for _, ed := range edgeDevs { + if err := ds.Client.Create(context.TODO(), ed); err != nil { + if apierrors.IsAlreadyExists(err) { + klog.V(5).InfoS("DeviceService already exist on Kubernetes", + "DeviceService", strings.ToLower(ed.Name)) + continue + } + klog.InfoS("created deviceService failed:", "DeviceService", strings.ToLower(ed.Name)) + return err + } + } + return nil +} + +// deleteDeviceServices deletes redundant deviceServices on OpenYurt +func (ds *DeviceServiceSyncer) deleteDeviceServices(redundantKubeDeviceServices map[string]*iotv1alpha1.DeviceService) error { + for _, kds := range redundantKubeDeviceServices { + if err := ds.Client.Delete(context.TODO(), kds); err != nil { + klog.V(5).ErrorS(err, "fail to delete the DeviceService on Kubernetes", + "DeviceService", kds.Name) + return err + } + } + return nil +} + +// updateDeviceServices updates deviceServices status on OpenYurt +func (ds *DeviceServiceSyncer) updateDeviceServices(syncedDeviceServices map[string]*iotv1alpha1.DeviceService) error { + for _, sd := range syncedDeviceServices { + if sd.ObjectMeta.ResourceVersion == "" { + continue + } + if err := ds.Client.Status().Update(context.TODO(), sd); err != nil { + if apierrors.IsConflict(err) { + klog.V(5).InfoS("update Conflicts", "DeviceService", sd.Name) + continue + } + klog.V(5).ErrorS(err, "fail to update the DeviceService on Kubernetes", + "DeviceService", sd.Name) + return err + } + } + return nil +} + +// completeCreateContent completes the content of the deviceService which will be created on OpenYurt +func (ds *DeviceServiceSyncer) completeCreateContent(edgeDS *iotv1alpha1.DeviceService) *iotv1alpha1.DeviceService { + createDevice := edgeDS.DeepCopy() + createDevice.Spec.NodePool = ds.NodePool + createDevice.Namespace = ds.Namespace + createDevice.Name = strings.Join([]string{ds.NodePool, edgeDS.Name}, "-") + createDevice.Spec.Managed = false + return createDevice +} + +// completeUpdateContent completes the content of the deviceService which will be updated on OpenYurt +func (ds *DeviceServiceSyncer) completeUpdateContent(kubeDS *iotv1alpha1.DeviceService, edgeDS *iotv1alpha1.DeviceService) *iotv1alpha1.DeviceService { + updatedDS := kubeDS.DeepCopy() + // update device status + updatedDS.Status.LastConnected = edgeDS.Status.LastConnected + updatedDS.Status.LastReported = edgeDS.Status.LastReported + updatedDS.Status.AdminState = edgeDS.Status.AdminState + return updatedDS +} diff --git a/pkg/yurtiotdock/controllers/predicate.go b/pkg/yurtiotdock/controllers/predicate.go new file mode 100644 index 00000000000..0c6dc5eadb5 --- /dev/null +++ b/pkg/yurtiotdock/controllers/predicate.go @@ -0,0 +1,48 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" +) + +func genFirstUpdateFilter(objKind string) predicate.Predicate { + return predicate.Funcs{ + // ignore the update event that is generated due to a + // new deviceprofile being added to the Edgex Foundry + UpdateFunc: func(e event.UpdateEvent) bool { + oldDp, ok := e.ObjectOld.(edgexCli.EdgeXObject) + if !ok { + klog.Infof("fail to assert object to deviceprofile, object kind is %s", objKind) + return false + } + newDp, ok := e.ObjectNew.(edgexCli.EdgeXObject) + if !ok { + klog.Infof("fail to assert object to deviceprofile, object kind is %s", objKind) + return false + } + if !oldDp.IsAddedToEdgeX() && newDp.IsAddedToEdgeX() { + return false + } + return true + }, + } +} diff --git a/pkg/yurtiotdock/controllers/util/conditions.go b/pkg/yurtiotdock/controllers/util/conditions.go new file mode 100644 index 00000000000..db892cc1459 --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/conditions.go @@ -0,0 +1,120 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +// NewDeviceCondition creates a new Device condition. +func NewDeviceCondition(condType iotv1alpha1.DeviceConditionType, status corev1.ConditionStatus, reason, message string) *iotv1alpha1.DeviceCondition { + return &iotv1alpha1.DeviceCondition{ + Type: condType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } +} + +// GetDeviceCondition returns the condition with the provided type. +func GetDeviceCondition(status iotv1alpha1.DeviceStatus, condType iotv1alpha1.DeviceConditionType) *iotv1alpha1.DeviceCondition { + for i := range status.Conditions { + c := status.Conditions[i] + if c.Type == condType { + return &c + } + } + return nil +} + +// SetDeviceCondition updates the Device to include the provided condition. If the condition that +// we are about to add already exists and has the same status, reason and message then we are not going to update. +func SetDeviceCondition(status *iotv1alpha1.DeviceStatus, condition *iotv1alpha1.DeviceCondition) { + currentCond := GetDeviceCondition(*status, condition.Type) + if currentCond != nil && currentCond.Status == condition.Status && currentCond.Reason == condition.Reason { + return + } + + if currentCond != nil && currentCond.Status == condition.Status { + condition.LastTransitionTime = currentCond.LastTransitionTime + } + newConditions := filterOutDeviceCondition(status.Conditions, condition.Type) + status.Conditions = append(newConditions, *condition) +} + +func filterOutDeviceCondition(conditions []iotv1alpha1.DeviceCondition, condType iotv1alpha1.DeviceConditionType) []iotv1alpha1.DeviceCondition { + var newConditions []iotv1alpha1.DeviceCondition + for _, c := range conditions { + if c.Type == condType { + continue + } + newConditions = append(newConditions, c) + } + return newConditions +} + +// NewDeviceServiceCondition creates a new DeviceService condition. +func NewDeviceServiceCondition(condType iotv1alpha1.DeviceServiceConditionType, status corev1.ConditionStatus, reason, message string) *iotv1alpha1.DeviceServiceCondition { + return &iotv1alpha1.DeviceServiceCondition{ + Type: condType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } +} + +// GetDeviceServiceCondition returns the condition with the provided type. +func GetDeviceServiceCondition(status iotv1alpha1.DeviceServiceStatus, condType iotv1alpha1.DeviceServiceConditionType) *iotv1alpha1.DeviceServiceCondition { + for i := range status.Conditions { + c := status.Conditions[i] + if c.Type == condType { + return &c + } + } + return nil +} + +// SetDeviceServiceCondition updates the DeviceService to include the provided condition. If the condition that +// we are about to add already exists and has the same status, reason and message then we are not going to update. +func SetDeviceServiceCondition(status *iotv1alpha1.DeviceServiceStatus, condition *iotv1alpha1.DeviceServiceCondition) { + currentCond := GetDeviceServiceCondition(*status, condition.Type) + if currentCond != nil && currentCond.Status == condition.Status && currentCond.Reason == condition.Reason { + return + } + + if currentCond != nil && currentCond.Status == condition.Status { + condition.LastTransitionTime = currentCond.LastTransitionTime + } + newConditions := filterOutDeviceServiceCondition(status.Conditions, condition.Type) + status.Conditions = append(newConditions, *condition) +} + +func filterOutDeviceServiceCondition(conditions []iotv1alpha1.DeviceServiceCondition, condType iotv1alpha1.DeviceServiceConditionType) []iotv1alpha1.DeviceServiceCondition { + var newConditions []iotv1alpha1.DeviceServiceCondition + for _, c := range conditions { + if c.Type == condType { + continue + } + newConditions = append(newConditions, c) + } + return newConditions +} diff --git a/pkg/yurtiotdock/controllers/util/fieldindexer.go b/pkg/yurtiotdock/controllers/util/fieldindexer.go new file mode 100644 index 00000000000..8245d343274 --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/fieldindexer.go @@ -0,0 +1,62 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "sync" + + "sigs.k8s.io/controller-runtime/pkg/client" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +const ( + IndexerPathForNodepool = "spec.nodePool" +) + +var registerOnce sync.Once + +func RegisterFieldIndexers(fi client.FieldIndexer) error { + var err error + registerOnce.Do(func() { + // register the fieldIndexer for device + if err = fi.IndexField(context.TODO(), &iotv1alpha1.Device{}, IndexerPathForNodepool, func(rawObj client.Object) []string { + device := rawObj.(*iotv1alpha1.Device) + return []string{device.Spec.NodePool} + }); err != nil { + return + } + + // register the fieldIndexer for deviceService + if err = fi.IndexField(context.TODO(), &iotv1alpha1.DeviceService{}, IndexerPathForNodepool, func(rawObj client.Object) []string { + deviceService := rawObj.(*iotv1alpha1.DeviceService) + return []string{deviceService.Spec.NodePool} + }); err != nil { + return + } + + // register the fieldIndexer for deviceProfile + if err = fi.IndexField(context.TODO(), &iotv1alpha1.DeviceProfile{}, IndexerPathForNodepool, func(rawObj client.Object) []string { + profile := rawObj.(*iotv1alpha1.DeviceProfile) + return []string{profile.Spec.NodePool} + }); err != nil { + return + } + }) + return err +} diff --git a/pkg/yurtiotdock/controllers/util/string.go b/pkg/yurtiotdock/controllers/util/string.go new file mode 100644 index 00000000000..4e7607a4d56 --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/string.go @@ -0,0 +1,30 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +// IsInStringLst checks if 'str' is in the 'strLst' +func IsInStringLst(strLst []string, str string) bool { + if len(strLst) == 0 { + return false + } + for _, s := range strLst { + if str == s { + return true + } + } + return false +} diff --git a/pkg/yurtiotdock/controllers/util/string_test.go b/pkg/yurtiotdock/controllers/util/string_test.go new file mode 100644 index 00000000000..7f56226ad8d --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/string_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" +) + +func TestIsInStringLst(t *testing.T) { + tests := []struct { + desc string + sl []string + s string + res bool + }{ + { + "test empty list", + []string{}, + "a", + false, + }, + { + "test not in list", + []string{"a", "b", "c"}, + "d", + false, + }, + { + "test not in list with one element", + []string{"a"}, + "b", + false, + }, + { + "test in list with one element", + []string{"aaa"}, + "aaa", + true, + }, + { + "test in list with one element", + []string{"aaa", "a", "bbb"}, + "a", + true, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + res := IsInStringLst(tt.sl, tt.s) + if res != tt.res { + t.Errorf("expect %v, but %v returned", tt.res, res) + } + }) + } +} diff --git a/pkg/yurtiotdock/controllers/util/tools.go b/pkg/yurtiotdock/controllers/util/tools.go new file mode 100644 index 00000000000..a95dc50395a --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/tools.go @@ -0,0 +1,99 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "fmt" + "io/ioutil" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +const ( + PODHOSTNAME = "/etc/hostname" + PODNAMESPACE = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" +) + +// GetNodePool get nodepool where yurt-iot-dock run +func GetNodePool(cfg *rest.Config) (string, error) { + var nodePool string + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nodePool, err + } + + bn, err := ioutil.ReadFile(PODHOSTNAME) + if err != nil { + return nodePool, fmt.Errorf("Read file %s fail: %v", PODHOSTNAME, err) + } + bns, err := ioutil.ReadFile(PODNAMESPACE) + if err != nil { + return nodePool, fmt.Errorf("Read file %s fail: %v", PODNAMESPACE, err) + } + name := strings.Replace(string(bn), "\n", "", -1) + namespace := string(bns) + + pod, err := client.CoreV1().Pods(namespace).Get(context.Background(), name, metav1.GetOptions{}) + if err != nil { + return nodePool, fmt.Errorf("not found pod %s/%s: %v", namespace, name, err) + } + node, err := client.CoreV1().Nodes().Get(context.Background(), pod.Spec.NodeName, metav1.GetOptions{}) + if err != nil { + return nodePool, fmt.Errorf("not found node %s: %v", pod.Spec.NodeName, err) + } + nodePool, ok := node.Labels["apps.openyurt.io/nodepool"] + if !ok { + return nodePool, fmt.Errorf("node %s doesn't add to a nodepool", node.GetName()) + } + return nodePool, err +} + +func GetEdgeDeviceServiceName(ds *iotv1alpha1.DeviceService, label string) string { + var actualDSName string + if _, ok := ds.ObjectMeta.Labels[label]; ok { + actualDSName = ds.ObjectMeta.Labels[label] + } else { + actualDSName = ds.GetName() + } + return actualDSName +} + +func GetEdgeDeviceName(d *iotv1alpha1.Device, label string) string { + var actualDeviceName string + if _, ok := d.ObjectMeta.Labels[label]; ok { + actualDeviceName = d.ObjectMeta.Labels[label] + } else { + actualDeviceName = d.GetName() + } + return actualDeviceName +} + +func GetEdgeDeviceProfileName(dp *iotv1alpha1.DeviceProfile, label string) string { + var actualDPName string + if _, ok := dp.ObjectMeta.Labels[label]; ok { + actualDPName = dp.ObjectMeta.Labels[label] + } else { + actualDPName = dp.GetName() + } + return actualDPName +} diff --git a/pkg/yurtiotdock/controllers/util/tools_test.go b/pkg/yurtiotdock/controllers/util/tools_test.go new file mode 100644 index 00000000000..fde94c4fffa --- /dev/null +++ b/pkg/yurtiotdock/controllers/util/tools_test.go @@ -0,0 +1,55 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/client-go/rest" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" +) + +func TestGetNodePool(t *testing.T) { + cfg := &rest.Config{} + res, err := GetNodePool(cfg) + if res != "" { + t.Errorf("expect nil on null config") + } + if err == nil { + t.Errorf("null config must cause error") + } +} + +func TestGetEdgeDeviceServiceName(t *testing.T) { + d := &iotv1alpha1.DeviceService{} + assert.Equal(t, GetEdgeDeviceServiceName(d, ""), "") + assert.Equal(t, GetEdgeDeviceServiceName(d, "a"), "") +} + +func TestGetEdgeDeviceName(t *testing.T) { + d := &iotv1alpha1.Device{} + assert.Equal(t, GetEdgeDeviceName(d, ""), "") + assert.Equal(t, GetEdgeDeviceName(d, "a"), "") +} + +func TestGetEdgeDeviceProfileName(t *testing.T) { + d := &iotv1alpha1.DeviceProfile{} + assert.Equal(t, GetEdgeDeviceProfileName(d, ""), "") + assert.Equal(t, GetEdgeDeviceProfileName(d, "a"), "") +} diff --git a/pkg/yurtiotdock/controllers/well_known_labels.go b/pkg/yurtiotdock/controllers/well_known_labels.go new file mode 100644 index 00000000000..fd414f13c9e --- /dev/null +++ b/pkg/yurtiotdock/controllers/well_known_labels.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +const ( + EdgeXObjectName = "yurt-iot-dock/edgex-object.name" +) From 3595932adc94859a3e70ddf8613e01a208dd4668 Mon Sep 17 00:00:00 2001 From: wesleysu <59680532+River-sh@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:38:22 +0800 Subject: [PATCH 64/93] add new gateway version (#1641) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 珩轩 --- .../crds/raven.openyurt.io_gateways.yaml | 195 ++++++++++++++++ .../yurt-manager-auto-generated.yaml | 12 +- pkg/apis/addtoscheme_raven_v1beta1.go | 26 +++ .../apps/v1alpha1/zz_generated.deepcopy.go | 2 +- .../apps/v1beta1/zz_generated.deepcopy.go | 2 +- pkg/apis/raven/v1alpha1/gateway_conversion.go | 94 +++++++- .../raven/v1alpha1/zz_generated.deepcopy.go | 2 +- pkg/apis/raven/v1beta1/default.go | 43 ++++ pkg/apis/raven/v1beta1/gateway_conversion.go | 48 ++++ pkg/apis/raven/v1beta1/gateway_types.go | 139 +++++++++++ pkg/apis/raven/v1beta1/groupversion_info.go | 44 ++++ .../raven/v1beta1/zz_generated.deepcopy.go | 220 ++++++++++++++++++ pkg/controller/raven/common.go | 2 +- .../gateway/v1alpha1/gateway_default.go | 4 +- .../gateway/v1alpha1/gateway_handler.go | 3 - .../gateway/v1beta1/gateway_default.go | 39 ++++ .../gateway/v1beta1/gateway_handler.go | 56 +++++ .../gateway/v1beta1/gateway_validation.go | 128 ++++++++++ pkg/webhook/server.go | 4 +- 19 files changed, 1038 insertions(+), 25 deletions(-) create mode 100644 pkg/apis/addtoscheme_raven_v1beta1.go create mode 100644 pkg/apis/raven/v1beta1/default.go create mode 100644 pkg/apis/raven/v1beta1/gateway_conversion.go create mode 100644 pkg/apis/raven/v1beta1/gateway_types.go create mode 100644 pkg/apis/raven/v1beta1/groupversion_info.go create mode 100644 pkg/apis/raven/v1beta1/zz_generated.deepcopy.go create mode 100644 pkg/webhook/gateway/v1beta1/gateway_default.go create mode 100644 pkg/webhook/gateway/v1beta1/gateway_handler.go create mode 100644 pkg/webhook/gateway/v1beta1/gateway_validation.go diff --git a/charts/yurt-manager/crds/raven.openyurt.io_gateways.yaml b/charts/yurt-manager/crds/raven.openyurt.io_gateways.yaml index 0924060e97d..1fbbefafb3f 100644 --- a/charts/yurt-manager/crds/raven.openyurt.io_gateways.yaml +++ b/charts/yurt-manager/crds/raven.openyurt.io_gateways.yaml @@ -159,6 +159,201 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - name: v1beta1 + schema: + openAPIV3Schema: + description: Gateway is the Schema for the gateways API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: GatewaySpec defines the desired state of Gateway + properties: + endpoints: + description: Endpoints are a list of available Endpoint. + items: + description: Endpoint stores all essential data for establishing + the VPN tunnel and Proxy + properties: + config: + additionalProperties: + type: string + description: Config is a map to record config for the raven + agent of node + type: object + nodeName: + description: NodeName is the Node hosting this endpoint. + type: string + port: + description: Port is the exposed port of the node + type: integer + publicIP: + description: PublicIP is the exposed IP of the node + type: string + type: + description: Type is the service type of the node, proxy or + tunnel + type: string + underNAT: + description: UnderNAT indicates whether node is under NAT + type: boolean + required: + - nodeName + - type + type: object + type: array + exposeType: + description: ExposeType determines how the Gateway is exposed. + type: string + nodeSelector: + description: NodeSelector is a label query over nodes that managed + by the gateway. The nodes in the same gateway should share same + layer 3 network. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + proxyConfig: + description: ProxyConfig determine the l7 proxy configuration + properties: + Replicas: + description: Replicas is the number of gateway active endpoints + that enabled proxy + type: integer + proxyHTTPPort: + description: ProxyHTTPPort is the proxy http port of the cross-domain + request + type: string + proxyHTTPSPort: + description: ProxyHTTPSPort is the proxy https port of the cross-domain + request + type: string + required: + - Replicas + type: object + tunnelConfig: + description: TunnelConfig determine the l3 tunnel configuration + properties: + Replicas: + description: Replicas is the number of gateway active endpoints + that enabled tunnel + type: integer + required: + - Replicas + type: object + type: object + status: + description: GatewayStatus defines the observed state of Gateway + properties: + activeEndpoints: + description: ActiveEndpoints is the reference of the active endpoint. + items: + description: Endpoint stores all essential data for establishing + the VPN tunnel and Proxy + properties: + config: + additionalProperties: + type: string + description: Config is a map to record config for the raven + agent of node + type: object + nodeName: + description: NodeName is the Node hosting this endpoint. + type: string + port: + description: Port is the exposed port of the node + type: integer + publicIP: + description: PublicIP is the exposed IP of the node + type: string + type: + description: Type is the service type of the node, proxy or + tunnel + type: string + underNAT: + description: UnderNAT indicates whether node is under NAT + type: boolean + required: + - nodeName + - type + type: object + type: array + nodes: + description: Nodes contains all information of nodes managed by Gateway. + items: + description: NodeInfo stores information of node managed by Gateway. + properties: + nodeName: + description: NodeName is the Node host name. + type: string + privateIP: + description: PrivateIP is the node private ip address + type: string + subnets: + description: Subnets is the pod ip range of the node + items: + type: string + type: array + required: + - nodeName + - privateIP + - subnets + type: object + type: array + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index 72e35edad21..177e5500223 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -522,14 +522,14 @@ webhooks: service: name: yurt-manager-webhook-service namespace: {{ .Release.Namespace }} - path: /mutate-raven-openyurt-io-v1alpha1-gateway + path: /mutate-raven-openyurt-io-v1beta1-gateway failurePolicy: Fail - name: mutate.raven.v1alpha1.gateway.openyurt.io + name: mutate.gateway.v1beta1.raven.openyurt.io rules: - apiGroups: - raven.openyurt.io apiVersions: - - v1alpha1 + - v1beta1 operations: - CREATE - UPDATE @@ -652,14 +652,14 @@ webhooks: service: name: yurt-manager-webhook-service namespace: {{ .Release.Namespace }} - path: /validate-raven-openyurt-io-v1alpha1-gateway + path: /validate-raven-openyurt-io-v1beta1-gateway failurePolicy: Fail - name: validate.raven.v1alpha1.gateway.openyurt.io + name: validate.gateway.v1beta1.raven.openyurt.io rules: - apiGroups: - raven.openyurt.io apiVersions: - - v1alpha1 + - v1beta1 operations: - CREATE - UPDATE diff --git a/pkg/apis/addtoscheme_raven_v1beta1.go b/pkg/apis/addtoscheme_raven_v1beta1.go new file mode 100644 index 00000000000..f5f8a575d08 --- /dev/null +++ b/pkg/apis/addtoscheme_raven_v1beta1.go @@ -0,0 +1,26 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apis + +import ( + version "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, version.SchemeBuilder.AddToScheme) +} diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go index a1352c1b16f..f732bdcaa9b 100644 --- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" ) diff --git a/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go b/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go index a62c4266aa2..ebb32758258 100644 --- a/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1beta1 import ( corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/pkg/apis/raven/v1alpha1/gateway_conversion.go b/pkg/apis/raven/v1alpha1/gateway_conversion.go index ebcce13d9dc..6192bfc4a21 100644 --- a/pkg/apis/raven/v1alpha1/gateway_conversion.go +++ b/pkg/apis/raven/v1alpha1/gateway_conversion.go @@ -16,6 +16,13 @@ limitations under the License. package v1alpha1 +import ( + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/conversion" + + "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" +) + /* Implementing the hub method is pretty easy -- we just have to add an empty method called Hub() to serve as a @@ -31,15 +38,86 @@ method called Hub() to serve as a // NOTE !!!!!!! @kadisi // If this version is not storageversion, you need to implement the ConvertTo and ConvertFrom methods -// need import "sigs.k8s.io/controller-runtime/pkg/conversion" -//func (src *Gateway) ConvertTo(dstRaw conversion.Hub) error { -// return nil -//} +func (src *Gateway) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*v1beta1.Gateway) + dst.ObjectMeta = src.ObjectMeta + if src.Spec.NodeSelector != nil { + dst.Spec.NodeSelector = src.Spec.NodeSelector + } + dst.Spec.ExposeType = string(src.Spec.ExposeType) + dst.Spec.TunnelConfig.Replicas = 1 + dst.Spec.ProxyConfig.Replicas = 1 + for _, eps := range src.Spec.Endpoints { + dst.Spec.Endpoints = append(dst.Spec.Endpoints, v1beta1.Endpoint{ + NodeName: eps.NodeName, + PublicIP: eps.PublicIP, + UnderNAT: eps.UnderNAT, + Config: eps.Config, + Type: v1beta1.Tunnel, + Port: v1beta1.DefaultTunnelServerExposedPort, + }) + } + for _, node := range src.Status.Nodes { + dst.Status.Nodes = append(dst.Status.Nodes, v1beta1.NodeInfo{ + NodeName: node.NodeName, + PrivateIP: node.PrivateIP, + Subnets: node.Subnets, + }) + } + if src.Status.ActiveEndpoint != nil { + dst.Status.ActiveEndpoints = []*v1beta1.Endpoint{ + { + NodeName: src.Status.ActiveEndpoint.NodeName, + PublicIP: src.Status.ActiveEndpoint.PublicIP, + UnderNAT: src.Status.ActiveEndpoint.UnderNAT, + Config: src.Status.ActiveEndpoint.Config, + Type: v1beta1.Tunnel, + Port: v1beta1.DefaultTunnelServerExposedPort, + }, + } + } + + klog.Infof("convert from v1alpha1 to v1beta1 for %s", dst.Name) + return nil +} // NOTE !!!!!!! @kadisi // If this version is not storageversion, you need to implement the ConvertTo and ConvertFrom methods -// need import "sigs.k8s.io/controller-runtime/pkg/conversion" -//func (dst *Gateway) ConvertFrom(srcRaw conversion.Hub) error { -// return nil -//} +func (dst *Gateway) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*v1beta1.Gateway) + dst.ObjectMeta = src.ObjectMeta + dst.Spec.NodeSelector = src.Spec.NodeSelector + dst.Spec.ExposeType = ExposeType(src.Spec.ExposeType) + for _, eps := range src.Spec.Endpoints { + dst.Spec.Endpoints = append(dst.Spec.Endpoints, Endpoint{ + NodeName: eps.NodeName, + PublicIP: eps.PublicIP, + UnderNAT: eps.UnderNAT, + Config: eps.Config, + }) + } + for _, node := range src.Status.Nodes { + dst.Status.Nodes = append(dst.Status.Nodes, NodeInfo{ + NodeName: node.NodeName, + PrivateIP: node.PrivateIP, + Subnets: node.Subnets, + }) + } + if src.Status.ActiveEndpoints == nil { + klog.Infof("convert from v1beta1 to v1alpha1 for %s", dst.Name) + return nil + } + if len(src.Status.ActiveEndpoints) < 1 { + dst.Status.ActiveEndpoint = nil + } else { + dst.Status.ActiveEndpoint = &Endpoint{ + NodeName: src.Status.ActiveEndpoints[0].NodeName, + PublicIP: src.Status.ActiveEndpoints[0].PublicIP, + UnderNAT: src.Status.ActiveEndpoints[0].UnderNAT, + Config: src.Status.ActiveEndpoints[0].Config, + } + } + klog.Infof("convert from v1beta1 to v1alpha1 for %s", dst.Name) + return nil +} diff --git a/pkg/apis/raven/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/raven/v1alpha1/zz_generated.deepcopy.go index d2d42342df3..c736ea956f0 100644 --- a/pkg/apis/raven/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/raven/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ limitations under the License. package v1alpha1 import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/pkg/apis/raven/v1beta1/default.go b/pkg/apis/raven/v1beta1/default.go new file mode 100644 index 00000000000..cbbea9a915f --- /dev/null +++ b/pkg/apis/raven/v1beta1/default.go @@ -0,0 +1,43 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openyurtio/openyurt/pkg/apis/raven" +) + +// SetDefaultsGateway set default values for Gateway. +func SetDefaultsGateway(obj *Gateway) { + // Set default value for Gateway + obj.Spec.NodeSelector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + raven.LabelCurrentGateway: obj.Name, + }, + } + for idx, val := range obj.Spec.Endpoints { + if val.Port == 0 { + switch val.Type { + case Proxy: + obj.Spec.Endpoints[idx].Port = DefaultProxyServerExposedPort + case Tunnel: + obj.Spec.Endpoints[idx].Port = DefaultTunnelServerExposedPort + } + } + } +} diff --git a/pkg/apis/raven/v1beta1/gateway_conversion.go b/pkg/apis/raven/v1beta1/gateway_conversion.go new file mode 100644 index 00000000000..4a87604e040 --- /dev/null +++ b/pkg/apis/raven/v1beta1/gateway_conversion.go @@ -0,0 +1,48 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +/* +Implementing the hub method is pretty easy -- we just have to add an empty +method called Hub() to serve as a +[marker](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub). +*/ + +// NOTE !!!!!! @kadisi +// If this version is storageversion, you only need to uncommand this method + +// Hub marks this type as a conversion hub. +//func (*Gateway) Hub() {} + +// NOTE !!!!!!! @kadisi +// If this version is not storageversion, you need to implement the ConvertTo and ConvertFrom methods + +// need import "sigs.k8s.io/controller-runtime/pkg/conversion" +//func (src *Gateway) ConvertTo(dstRaw conversion.Hub) error { +// return nil +//} + +// NOTE !!!!!!! @kadisi +// If this version is not storageversion, you need to implement the ConvertTo and ConvertFrom methods + +// need import "sigs.k8s.io/controller-runtime/pkg/conversion" +//func (dst *Gateway) ConvertFrom(srcRaw conversion.Hub) error { +// return nil +//} + +// Hub marks this type as a conversion hub. +func (*Gateway) Hub() {} diff --git a/pkg/apis/raven/v1beta1/gateway_types.go b/pkg/apis/raven/v1beta1/gateway_types.go new file mode 100644 index 00000000000..a8d433c0a46 --- /dev/null +++ b/pkg/apis/raven/v1beta1/gateway_types.go @@ -0,0 +1,139 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// Event reason. +const ( + // EventActiveEndpointElected is the event indicating a new active endpoint is elected. + EventActiveEndpointElected = "ActiveEndpointElected" + // EventActiveEndpointLost is the event indicating the active endpoint is lost. + EventActiveEndpointLost = "ActiveEndpointLost" +) + +const ( + ExposeTypePublicIP = "PublicIP" + ExposeTypeLoadBalancer = "LoadBalancer" +) + +const ( + Proxy = "proxy" + Tunnel = "tunnel" + + DefaultProxyServerSecurePort = 10263 + DefaultProxyServerInsecurePort = 10264 + DefaultProxyServerExposedPort = 10262 + DefaultTunnelServerExposedPort = 4500 +) + +// ProxyConfiguration is the configuration for raven l7 proxy +type ProxyConfiguration struct { + // Replicas is the number of gateway active endpoints that enabled proxy + Replicas int `json:"Replicas"` + // ProxyHTTPPort is the proxy http port of the cross-domain request + ProxyHTTPPort string `json:"proxyHTTPPort,omitempty"` + // ProxyHTTPSPort is the proxy https port of the cross-domain request + ProxyHTTPSPort string `json:"proxyHTTPSPort,omitempty"` +} + +// TunnelConfiguration is the configuration for raven l3 tunnel +type TunnelConfiguration struct { + // Replicas is the number of gateway active endpoints that enabled tunnel + Replicas int `json:"Replicas"` +} + +// GatewaySpec defines the desired state of Gateway +type GatewaySpec struct { + // NodeSelector is a label query over nodes that managed by the gateway. + // The nodes in the same gateway should share same layer 3 network. + NodeSelector *metav1.LabelSelector `json:"nodeSelector,omitempty"` + // ProxyConfig determine the l7 proxy configuration + ProxyConfig ProxyConfiguration `json:"proxyConfig,omitempty"` + // TunnelConfig determine the l3 tunnel configuration + TunnelConfig TunnelConfiguration `json:"tunnelConfig,omitempty"` + // Endpoints are a list of available Endpoint. + Endpoints []Endpoint `json:"endpoints,omitempty"` + // ExposeType determines how the Gateway is exposed. + ExposeType string `json:"exposeType,omitempty"` +} + +// Endpoint stores all essential data for establishing the VPN tunnel and Proxy +type Endpoint struct { + // NodeName is the Node hosting this endpoint. + NodeName string `json:"nodeName"` + // Type is the service type of the node, proxy or tunnel + Type string `json:"type"` + // Port is the exposed port of the node + Port int `json:"port,omitempty"` + // UnderNAT indicates whether node is under NAT + UnderNAT bool `json:"underNAT,omitempty"` + // PublicIP is the exposed IP of the node + PublicIP string `json:"publicIP,omitempty"` + // Config is a map to record config for the raven agent of node + Config map[string]string `json:"config,omitempty"` +} + +// NodeInfo stores information of node managed by Gateway. +type NodeInfo struct { + // NodeName is the Node host name. + NodeName string `json:"nodeName"` + // PrivateIP is the node private ip address + PrivateIP string `json:"privateIP"` + // Subnets is the pod ip range of the node + Subnets []string `json:"subnets"` +} + +// GatewayStatus defines the observed state of Gateway +type GatewayStatus struct { + // Nodes contains all information of nodes managed by Gateway. + Nodes []NodeInfo `json:"nodes,omitempty"` + // ActiveEndpoints is the reference of the active endpoint. + ActiveEndpoints []*Endpoint `json:"activeEndpoints,omitempty"` +} + +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster,path=gateways,shortName=gw,categories=all +// +kubebuilder:storageversion + +// Gateway is the Schema for the gateways API +type Gateway struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GatewaySpec `json:"spec,omitempty"` + Status GatewayStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// GatewayList contains a list of Gateway +type GatewayList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Gateway `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Gateway{}, &GatewayList{}) +} diff --git a/pkg/apis/raven/v1beta1/groupversion_info.go b/pkg/apis/raven/v1beta1/groupversion_info.go new file mode 100644 index 00000000000..e35d10626d0 --- /dev/null +++ b/pkg/apis/raven/v1beta1/groupversion_info.go @@ -0,0 +1,44 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +// Package v1beta1 contains API Schema definitions for the raven v1beta1API group +// +kubebuilder:object:generate=true +// +groupName=raven.openyurt.io + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "raven.openyurt.io", Version: "v1beta1"} + + SchemeGroupVersion = GroupVersion + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Resource is required by pkg/client/listers/... +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/apis/raven/v1beta1/zz_generated.deepcopy.go b/pkg/apis/raven/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..99e1cbb33f2 --- /dev/null +++ b/pkg/apis/raven/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,220 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Endpoint) DeepCopyInto(out *Endpoint) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Endpoint. +func (in *Endpoint) DeepCopy() *Endpoint { + if in == nil { + return nil + } + out := new(Endpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Gateway) DeepCopyInto(out *Gateway) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Gateway. +func (in *Gateway) DeepCopy() *Gateway { + if in == nil { + return nil + } + out := new(Gateway) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Gateway) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayList) DeepCopyInto(out *GatewayList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Gateway, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayList. +func (in *GatewayList) DeepCopy() *GatewayList { + if in == nil { + return nil + } + out := new(GatewayList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GatewayList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewaySpec) DeepCopyInto(out *GatewaySpec) { + *out = *in + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + out.ProxyConfig = in.ProxyConfig + out.TunnelConfig = in.TunnelConfig + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]Endpoint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewaySpec. +func (in *GatewaySpec) DeepCopy() *GatewaySpec { + if in == nil { + return nil + } + out := new(GatewaySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayStatus) DeepCopyInto(out *GatewayStatus) { + *out = *in + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]NodeInfo, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ActiveEndpoints != nil { + in, out := &in.ActiveEndpoints, &out.ActiveEndpoints + *out = make([]*Endpoint, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Endpoint) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayStatus. +func (in *GatewayStatus) DeepCopy() *GatewayStatus { + if in == nil { + return nil + } + out := new(GatewayStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeInfo) DeepCopyInto(out *NodeInfo) { + *out = *in + if in.Subnets != nil { + in, out := &in.Subnets, &out.Subnets + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeInfo. +func (in *NodeInfo) DeepCopy() *NodeInfo { + if in == nil { + return nil + } + out := new(NodeInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyConfiguration) DeepCopyInto(out *ProxyConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyConfiguration. +func (in *ProxyConfiguration) DeepCopy() *ProxyConfiguration { + if in == nil { + return nil + } + out := new(ProxyConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TunnelConfiguration) DeepCopyInto(out *TunnelConfiguration) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TunnelConfiguration. +func (in *TunnelConfiguration) DeepCopy() *TunnelConfiguration { + if in == nil { + return nil + } + out := new(TunnelConfiguration) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/raven/common.go b/pkg/controller/raven/common.go index e1af9b1184d..f9325510320 100644 --- a/pkg/controller/raven/common.go +++ b/pkg/controller/raven/common.go @@ -21,5 +21,5 @@ var ( ) const ( - ControllerName = "ravenl3" + ControllerName = "gateway" ) diff --git a/pkg/webhook/gateway/v1alpha1/gateway_default.go b/pkg/webhook/gateway/v1alpha1/gateway_default.go index d7766e1e856..8a55524a79f 100644 --- a/pkg/webhook/gateway/v1alpha1/gateway_default.go +++ b/pkg/webhook/gateway/v1alpha1/gateway_default.go @@ -28,12 +28,12 @@ import ( // Default satisfies the defaulting webhook interface. func (webhook *GatewayHandler) Default(ctx context.Context, obj runtime.Object) error { - np, ok := obj.(*v1alpha1.Gateway) + gw, ok := obj.(*v1alpha1.Gateway) if !ok { return apierrors.NewBadRequest(fmt.Sprintf("expected a Gateway but got a %T", obj)) } - v1alpha1.SetDefaultsGateway(np) + v1alpha1.SetDefaultsGateway(gw) return nil } diff --git a/pkg/webhook/gateway/v1alpha1/gateway_handler.go b/pkg/webhook/gateway/v1alpha1/gateway_handler.go index 61cecb7c0e3..97573b424a4 100644 --- a/pkg/webhook/gateway/v1alpha1/gateway_handler.go +++ b/pkg/webhook/gateway/v1alpha1/gateway_handler.go @@ -44,9 +44,6 @@ func (webhook *GatewayHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string Complete() } -// +kubebuilder:webhook:path=/validate-raven-openyurt-io-v1alpha1-gateway,mutating=false,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1;v1beta1,groups=raven.openyurt.io,resources=gateways,verbs=create;update,versions=v1alpha1,name=validate.raven.v1alpha1.gateway.openyurt.io -// +kubebuilder:webhook:path=/mutate-raven-openyurt-io-v1alpha1-gateway,mutating=true,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1;v1beta1,groups=raven.openyurt.io,resources=gateways,verbs=create;update,versions=v1alpha1,name=mutate.raven.v1alpha1.gateway.openyurt.io - // Cluster implements a validating and defaulting webhook for Cluster. type GatewayHandler struct { Client client.Client diff --git a/pkg/webhook/gateway/v1beta1/gateway_default.go b/pkg/webhook/gateway/v1beta1/gateway_default.go new file mode 100644 index 00000000000..c966416956c --- /dev/null +++ b/pkg/webhook/gateway/v1beta1/gateway_default.go @@ -0,0 +1,39 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" +) + +// Default satisfies the defaulting webhook interface. +func (webhook *GatewayHandler) Default(ctx context.Context, obj runtime.Object) error { + gw, ok := obj.(*v1beta1.Gateway) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a Gateway but got a %T", obj)) + } + + v1beta1.SetDefaultsGateway(gw) + + return nil +} diff --git a/pkg/webhook/gateway/v1beta1/gateway_handler.go b/pkg/webhook/gateway/v1beta1/gateway_handler.go new file mode 100644 index 00000000000..1cd33b5540a --- /dev/null +++ b/pkg/webhook/gateway/v1beta1/gateway_handler.go @@ -0,0 +1,56 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + "github.com/openyurtio/openyurt/pkg/webhook/util" +) + +// SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error +func (webhook *GatewayHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { + // init + webhook.Client = mgr.GetClient() + + gvk, err := apiutil.GVKForObject(&v1beta1.Gateway{}, mgr.GetScheme()) + if err != nil { + return "", "", err + } + return util.GenerateMutatePath(gvk), + util.GenerateValidatePath(gvk), + ctrl.NewWebhookManagedBy(mgr). + For(&v1beta1.Gateway{}). + WithDefaulter(webhook). + WithValidator(webhook). + Complete() +} + +// +kubebuilder:webhook:path=/validate-raven-openyurt-io-v1beta1-gateway,mutating=false,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1;v1beta1,groups=raven.openyurt.io,resources=gateways,verbs=create;update,versions=v1beta1,name=validate.gateway.v1beta1.raven.openyurt.io +// +kubebuilder:webhook:path=/mutate-raven-openyurt-io-v1beta1-gateway,mutating=true,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1;v1beta1,groups=raven.openyurt.io,resources=gateways,verbs=create;update,versions=v1beta1,name=mutate.gateway.v1beta1.raven.openyurt.io + +// Cluster implements a validating and defaulting webhook for Cluster. +type GatewayHandler struct { + Client client.Client +} + +var _ webhook.CustomDefaulter = &GatewayHandler{} +var _ webhook.CustomValidator = &GatewayHandler{} diff --git a/pkg/webhook/gateway/v1beta1/gateway_validation.go b/pkg/webhook/gateway/v1beta1/gateway_validation.go new file mode 100644 index 00000000000..6c9008cb53c --- /dev/null +++ b/pkg/webhook/gateway/v1beta1/gateway_validation.go @@ -0,0 +1,128 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "context" + "fmt" + "net" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" +) + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *GatewayHandler) ValidateCreate(ctx context.Context, obj runtime.Object) error { + gw, ok := obj.(*v1beta1.Gateway) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a Gateway but got a %T", obj)) + } + + return validate(gw) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *GatewayHandler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error { + newGw, ok := newObj.(*v1beta1.Gateway) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a Gateway but got a %T", newObj)) + } + oldGw, ok := oldObj.(*v1beta1.Gateway) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a Gateway} but got a %T", oldObj)) + } + + if newGw.GetName() != oldGw.GetName() { + return apierrors.NewBadRequest(fmt.Sprintf("gateway name can not change")) + } + if err := validate(newGw); err != nil { + return err + } + + return nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. +func (webhook *GatewayHandler) ValidateDelete(_ context.Context, obj runtime.Object) error { + return nil +} + +func validate(g *v1beta1.Gateway) error { + var errList field.ErrorList + + if g.Spec.ExposeType != "" { + if g.Spec.ExposeType != v1beta1.ExposeTypeLoadBalancer && g.Spec.ExposeType != v1beta1.ExposeTypePublicIP { + fldPath := field.NewPath("spec").Child("exposeType") + errList = append(errList, field.Invalid(fldPath, g.Spec.ExposeType, "the 'exposeType' field is irregularity")) + } + if g.Spec.ExposeType == v1beta1.ExposeTypeLoadBalancer || g.Spec.ExposeType == v1beta1.ExposeTypePublicIP { + for i, ep := range g.Spec.Endpoints { + if ep.UnderNAT { + fldPath := field.NewPath("spec").Child(fmt.Sprintf("endpoints[%d]", i)).Child("underNAT") + errList = append(errList, field.Invalid(fldPath, ep.UnderNAT, fmt.Sprintf("the 'underNAT' field for exposed gateway %s/%s must be false", g.Namespace, g.Name))) + } + } + } + } + + if len(g.Spec.Endpoints) != 0 { + underNAT := g.Spec.Endpoints[0].UnderNAT + for i, ep := range g.Spec.Endpoints { + if ep.UnderNAT != underNAT { + fldPath := field.NewPath("spec").Child(fmt.Sprintf("endpoints[%d]", i)).Child("underNAT") + errList = append(errList, field.Invalid(fldPath, ep.UnderNAT, "the 'underNAT' field in endpoints must be the same")) + } + if ep.PublicIP != "" { + if err := validateIP(ep.PublicIP); err != nil { + fldPath := field.NewPath("spec").Child(fmt.Sprintf("endpoints[%d]", i)).Child("publicIP") + errList = append(errList, field.Invalid(fldPath, ep.PublicIP, "the 'publicIP' field must be a validate IP address")) + } + } + if ep.Type != v1beta1.Tunnel && ep.Type != v1beta1.Proxy { + fldPath := field.NewPath("spec").Child(fmt.Sprintf("endpoints[%d]", i)).Child("type") + errList = append(errList, field.Invalid(fldPath, ep.Type, fmt.Sprintf("the 'type' field must be set %s or %s ", v1beta1.Tunnel, v1beta1.Proxy))) + } + if len(ep.NodeName) == 0 { + fldPath := field.NewPath("spec").Child(fmt.Sprintf("endpoints[%d]", i)).Child("nodeName") + errList = append(errList, field.Invalid(fldPath, ep.NodeName, "the 'nodeName' field must not be empty")) + } + } + } + + if errList != nil { + return apierrors.NewInvalid( + schema.GroupKind{Group: v1beta1.SchemeGroupVersion.Group, Kind: g.Kind}, + g.Name, errList) + } + + klog.Infof("Validate Gateway %s successfully ...", klog.KObj(g)) + + return nil +} + +func validateIP(ip string) error { + s := net.ParseIP(ip) + if s.To4() != nil || s.To16() != nil { + return nil + } + return fmt.Errorf("invalid ip address: %s", ip) +} diff --git a/pkg/webhook/server.go b/pkg/webhook/server.go index c22ffe2ef49..60278a30a2d 100644 --- a/pkg/webhook/server.go +++ b/pkg/webhook/server.go @@ -33,7 +33,7 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon" "github.com/openyurtio/openyurt/pkg/controller/yurtappset" "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset" - v1alpha1gateway "github.com/openyurtio/openyurt/pkg/webhook/gateway/v1alpha1" + v1beta1gateway "github.com/openyurtio/openyurt/pkg/webhook/gateway/v1beta1" v1node "github.com/openyurtio/openyurt/pkg/webhook/node/v1" v1alpha1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1alpha1" v1beta1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1beta1" @@ -73,7 +73,7 @@ func addControllerWebhook(name string, handler SetupWebhookWithManager) { } func init() { - addControllerWebhook(raven.ControllerName, &v1alpha1gateway.GatewayHandler{}) + addControllerWebhook(raven.ControllerName, &v1beta1gateway.GatewayHandler{}) addControllerWebhook(nodepool.ControllerName, &v1alpha1nodepool.NodePoolHandler{}) addControllerWebhook(nodepool.ControllerName, &v1beta1nodepool.NodePoolHandler{}) addControllerWebhook(yurtstaticset.ControllerName, &v1alpha1yurtstaticset.YurtStaticSetHandler{}) From 2ed7f0f8f699b4aa2011e333882ae9ec9337e050 Mon Sep 17 00:00:00 2001 From: Abyss <45425302+wangxye@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:36:23 +0800 Subject: [PATCH 65/93] reclaim device, deviceprofile and deviceservice (#1647) Signed-off-by: wangxye <1031989637@qq.com> --- cmd/yurt-iot-dock/app/core.go | 49 ++++++++++++++++++- .../controllers/device_controller.go | 25 ++++++++++ .../controllers/deviceprofile_controller.go | 25 ++++++++++ .../controllers/deviceservice_controller.go | 25 ++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/cmd/yurt-iot-dock/app/core.go b/cmd/yurt-iot-dock/app/core.go index 39e30bcaf30..3d4aae34219 100644 --- a/cmd/yurt-iot-dock/app/core.go +++ b/cmd/yurt-iot-dock/app/core.go @@ -20,6 +20,8 @@ import ( "context" "fmt" "os" + "os/signal" + "syscall" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -30,6 +32,7 @@ import ( "k8s.io/klog/v2" "k8s.io/klog/v2/klogr" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" @@ -181,12 +184,56 @@ func Run(opts *options.YurtIoTDockOptions, stopCh <-chan struct{}) { } setupLog.Info("[run controllers] Starting manager, acting on " + fmt.Sprintf("[NodePool: %s, Namespace: %s]", opts.Nodepool, opts.Namespace)) - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(SetupSignalHandler(mgr.GetClient(), opts)); err != nil { setupLog.Error(err, "failed to running manager") os.Exit(1) } } +func deleteCRsOnControllerShutdown(ctx context.Context, cli client.Client, opts *options.YurtIoTDockOptions) error { + setupLog.Info("[deleteCRsOnControllerShutdown] start delete device crd") + if err := controllers.DeleteDevicesOnControllerShutdown(ctx, cli, opts); err != nil { + setupLog.Error(err, "failed to shutdown device cr") + return err + } + + setupLog.Info("[deleteCRsOnControllerShutdown] start delete deviceprofile crd") + if err := controllers.DeleteDeviceProfilesOnControllerShutdown(ctx, cli, opts); err != nil { + setupLog.Error(err, "failed to shutdown deviceprofile cr") + return err + } + + setupLog.Info("[deleteCRsOnControllerShutdown] start delete deviceservice crd") + if err := controllers.DeleteDeviceServicesOnControllerShutdown(ctx, cli, opts); err != nil { + setupLog.Error(err, "failed to shutdown deviceservice cr") + return err + } + + return nil +} + +var onlyOneSignalHandler = make(chan struct{}) +var shutdownSignals = []os.Signal{syscall.SIGTERM} + +func SetupSignalHandler(client client.Client, opts *options.YurtIoTDockOptions) context.Context { + close(onlyOneSignalHandler) // panics when called twice + + ctx, cancel := context.WithCancel(context.Background()) + setupLog.Info("[SetupSignalHandler] shutdown controller with crd") + c := make(chan os.Signal, 2) + signal.Notify(c, shutdownSignals...) + go func() { + <-c + setupLog.Info("[SetupSignalHandler] shutdown signal concur") + deleteCRsOnControllerShutdown(ctx, client, opts) + cancel() + <-c + os.Exit(1) // second signal. Exit directly. + }() + + return ctx +} + func preflightCheck(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { client, err := kubernetes.NewForConfig(mgr.GetConfig()) if err != nil { diff --git a/pkg/yurtiotdock/controllers/device_controller.go b/pkg/yurtiotdock/controllers/device_controller.go index dd61d5a6a96..662feb3632d 100644 --- a/pkg/yurtiotdock/controllers/device_controller.go +++ b/pkg/yurtiotdock/controllers/device_controller.go @@ -29,6 +29,7 @@ import ( "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" @@ -293,3 +294,27 @@ func (r *DeviceReconciler) reconcileDeviceProperties(d *iotv1alpha1.Device, devi } return newDeviceStatus, failedPropertyNames } + +func DeleteDevicesOnControllerShutdown(ctx context.Context, cli client.Client, opts *options.YurtIoTDockOptions) error { + var deviceList iotv1alpha1.DeviceList + if err := cli.List(ctx, &deviceList, client.InNamespace(opts.Namespace)); err != nil { + return err + } + klog.V(4).Infof("DeviceList, successfully get the list") + + for _, device := range deviceList.Items { + controllerutil.RemoveFinalizer(&device, iotv1alpha1.DeviceFinalizer) + if err := cli.Update(ctx, &device); err != nil { + klog.Errorf("DeviceName: %s, update device err:%v", device.GetName(), err) + continue + } + + if err := cli.Delete(ctx, &device); err != nil { + klog.Errorf("DeviceName: %s, update device err:%v", device.GetName(), err) + continue + } + } + klog.V(4).Infof("DeviceList, successfully delete the list") + + return nil +} diff --git a/pkg/yurtiotdock/controllers/deviceprofile_controller.go b/pkg/yurtiotdock/controllers/deviceprofile_controller.go index d174cc2ebb6..459f39156b3 100644 --- a/pkg/yurtiotdock/controllers/deviceprofile_controller.go +++ b/pkg/yurtiotdock/controllers/deviceprofile_controller.go @@ -27,6 +27,7 @@ import ( "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" @@ -163,3 +164,27 @@ func (r *DeviceProfileReconciler) reconcileCreateDeviceProfile(ctx context.Conte dp.Status.Synced = true return r.Status().Update(ctx, dp) } + +func DeleteDeviceProfilesOnControllerShutdown(ctx context.Context, cli client.Client, opts *options.YurtIoTDockOptions) error { + var deviceProfileList iotv1alpha1.DeviceProfileList + if err := cli.List(ctx, &deviceProfileList, client.InNamespace(opts.Namespace)); err != nil { + return err + } + klog.V(4).Infof("DeviceProfileList, successfully get the list") + + for _, deviceProfile := range deviceProfileList.Items { + controllerutil.RemoveFinalizer(&deviceProfile, iotv1alpha1.DeviceProfileFinalizer) + if err := cli.Update(ctx, &deviceProfile); err != nil { + klog.Errorf("deviceProfileName: %s, update deviceProfile err:%v", deviceProfile.GetName(), err) + continue + } + + if err := cli.Delete(ctx, &deviceProfile); err != nil { + klog.Errorf("deviceProfileName: %s, update deviceProfile err:%v", deviceProfile.GetName(), err) + continue + } + } + klog.V(4).Infof("DeviceProfileList, successfully delete the list") + + return nil +} diff --git a/pkg/yurtiotdock/controllers/deviceservice_controller.go b/pkg/yurtiotdock/controllers/deviceservice_controller.go index 2822558b5f0..b924d789c5c 100644 --- a/pkg/yurtiotdock/controllers/deviceservice_controller.go +++ b/pkg/yurtiotdock/controllers/deviceservice_controller.go @@ -28,6 +28,7 @@ import ( "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" @@ -218,3 +219,27 @@ func (r *DeviceServiceReconciler) reconcileUpdateDeviceService(ctx context.Conte util.SetDeviceServiceCondition(deviceServiceStatus, util.NewDeviceServiceCondition(iotv1alpha1.DeviceServiceManagingCondition, corev1.ConditionTrue, "", "")) return nil } + +func DeleteDeviceServicesOnControllerShutdown(ctx context.Context, cli client.Client, opts *options.YurtIoTDockOptions) error { + var deviceServiceList iotv1alpha1.DeviceServiceList + if err := cli.List(ctx, &deviceServiceList, client.InNamespace(opts.Namespace)); err != nil { + return err + } + klog.V(4).Infof("DeviceServiceList, successfully get the list") + + for _, deviceService := range deviceServiceList.Items { + controllerutil.RemoveFinalizer(&deviceService, iotv1alpha1.DeviceServiceFinalizer) + if err := cli.Update(ctx, &deviceService); err != nil { + klog.Errorf("DeviceServiceName: %s, update deviceservice err:%v", deviceService.GetName(), err) + continue + } + + if err := cli.Delete(ctx, &deviceService); err != nil { + klog.Errorf("DeviceServiceName: %s, update deviceservice err:%v", deviceService.GetName(), err) + continue + } + } + klog.V(4).Infof("DeviceServiceList, successfully get the list") + + return nil +} From 3dbb153701f9de34bdb2b276f9196e8beb2d48ab Mon Sep 17 00:00:00 2001 From: rambohe Date: Tue, 15 Aug 2023 10:26:23 +0800 Subject: [PATCH 66/93] use dynamicinformerfactory to get nodepool and remove yurt-app-manager-api dependencies (#1652) --- cmd/yurthub/app/config/config.go | 100 +-- cmd/yurthub/app/start.go | 2 +- go.mod | 1 - go.sum | 9 - .../apps/well_known_labels_annotations.go | 1 + pkg/util/kubeconfig/kubeconfig.go | 17 - pkg/util/kubeconfig/kubeconfig_test.go | 2 - .../kubeadm/app/util/apiclient/idempotency.go | 32 +- pkg/yurtadm/cmd/join/join.go | 12 +- pkg/yurthub/filter/initializer/initializer.go | 18 +- .../filter/initializer/initializer_test.go | 21 +- pkg/yurthub/filter/manager/manager.go | 10 +- pkg/yurthub/filter/manager/manager_test.go | 19 +- .../filter/nodeportisolation/filter.go | 2 +- pkg/yurthub/filter/servicetopology/filter.go | 42 +- .../filter/servicetopology/filter_test.go | 591 +++++++++--------- 16 files changed, 423 insertions(+), 456 deletions(-) diff --git a/cmd/yurthub/app/config/config.go b/cmd/yurthub/app/config/config.go index 06529a69fc5..22255987b70 100644 --- a/cmd/yurthub/app/config/config.go +++ b/cmd/yurthub/app/config/config.go @@ -28,15 +28,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/apimachinery/pkg/watch" apiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/dynamiccertificates" apiserveroptions "k8s.io/apiserver/pkg/server/options" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/informers" coreinformers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - core "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" componentbaseconfig "k8s.io/component-base/config" @@ -47,18 +47,12 @@ import ( "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/certificate" certificatemgr "github.com/openyurtio/openyurt/pkg/yurthub/certificate/manager" - "github.com/openyurtio/openyurt/pkg/yurthub/filter" "github.com/openyurtio/openyurt/pkg/yurthub/filter/manager" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/meta" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer" "github.com/openyurtio/openyurt/pkg/yurthub/network" "github.com/openyurtio/openyurt/pkg/yurthub/storage/disk" "github.com/openyurtio/openyurt/pkg/yurthub/util" - yurtcorev1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" - yurtclientset "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned" - "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned/fake" - yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" - yurtv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions/apps/v1alpha1" ) // YurtHubConfiguration represents configuration of yurthub @@ -77,7 +71,7 @@ type YurtHubConfiguration struct { SerializerManager *serializer.SerializerManager RESTMapperManager *meta.RESTMapperManager SharedFactory informers.SharedInformerFactory - YurtSharedFactory yurtinformers.SharedInformerFactory + NodePoolInformerFactory dynamicinformer.DynamicSharedInformerFactory WorkingMode util.WorkingMode KubeletHealthGracePeriod time.Duration FilterManager *manager.Manager @@ -132,13 +126,13 @@ func Complete(options *options.YurtHubOptions) (*YurtHubConfiguration, error) { } workingMode := util.WorkingMode(options.WorkingMode) - proxiedClient, sharedFactory, yurtSharedFactory, err := createClientAndSharedInformers(fmt.Sprintf("http://%s:%d", options.YurtHubProxyHost, options.YurtHubProxyPort), options.EnableNodePool) + proxiedClient, sharedFactory, dynamicSharedFactory, err := createClientAndSharedInformers(fmt.Sprintf("http://%s:%d", options.YurtHubProxyHost, options.YurtHubProxyPort), options.NodePoolName) if err != nil { return nil, err } tenantNs := util.ParseTenantNsFromOrgs(options.YurtHubCertOrganizations) - registerInformers(options, sharedFactory, yurtSharedFactory, workingMode, tenantNs) - filterManager, err := manager.NewFilterManager(options, sharedFactory, yurtSharedFactory, proxiedClient, serializerManager, us[0].Host) + registerInformers(options, sharedFactory, workingMode, tenantNs) + filterManager, err := manager.NewFilterManager(options, sharedFactory, dynamicSharedFactory, proxiedClient, serializerManager, us[0].Host) if err != nil { klog.Errorf("could not create filter manager, %v", err) return nil, err @@ -160,7 +154,7 @@ func Complete(options *options.YurtHubOptions) (*YurtHubConfiguration, error) { SerializerManager: serializerManager, RESTMapperManager: restMapperManager, SharedFactory: sharedFactory, - YurtSharedFactory: yurtSharedFactory, + NodePoolInformerFactory: dynamicSharedFactory, KubeletHealthGracePeriod: options.KubeletHealthGracePeriod, FilterManager: filterManager, MinRequestTimeout: options.MinRequestTimeout, @@ -241,9 +235,8 @@ func parseRemoteServers(serverAddr string) ([]*url.URL, error) { } // createClientAndSharedInformers create kubeclient and sharedInformers from the given proxyAddr. -func createClientAndSharedInformers(proxyAddr string, enableNodePool bool) (kubernetes.Interface, informers.SharedInformerFactory, yurtinformers.SharedInformerFactory, error) { +func createClientAndSharedInformers(proxyAddr string, nodePoolName string) (kubernetes.Interface, informers.SharedInformerFactory, dynamicinformer.DynamicSharedInformerFactory, error) { var kubeConfig *rest.Config - var yurtClient yurtclientset.Interface var err error kubeConfig, err = clientcmd.BuildConfigFromFlags(proxyAddr, "") if err != nil { @@ -255,53 +248,27 @@ func createClientAndSharedInformers(proxyAddr string, enableNodePool bool) (kube return nil, nil, nil, err } - fakeYurtClient := &fake.Clientset{} - fakeWatch := watch.NewFake() - fakeYurtClient.AddWatchReactor("nodepools", core.DefaultWatchReactor(fakeWatch, nil)) - // init yurtClient by fake client - yurtClient = fakeYurtClient - if enableNodePool { - yurtClient, err = yurtclientset.NewForConfig(kubeConfig) - if err != nil { - return nil, nil, nil, err - } + dynamicClient, err := dynamic.NewForConfig(kubeConfig) + if err != nil { + return nil, nil, nil, err } - return client, informers.NewSharedInformerFactory(client, 24*time.Hour), - yurtinformers.NewSharedInformerFactory(yurtClient, 24*time.Hour), nil + dynamicInformerFactory := dynamicinformer.NewDynamicSharedInformerFactory(dynamicClient, 24*time.Hour) + if len(nodePoolName) != 0 { + dynamicInformerFactory = dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicClient, 24*time.Hour, metav1.NamespaceAll, func(options *metav1.ListOptions) { + options.FieldSelector = fields.Set{"metadata.name": nodePoolName}.String() + }) + } + + return client, informers.NewSharedInformerFactory(client, 24*time.Hour), dynamicInformerFactory, nil } -// registerInformers reconstruct node/nodePool/configmap informers +// registerInformers reconstruct configmap/secret/pod informers func registerInformers(options *options.YurtHubOptions, informerFactory informers.SharedInformerFactory, - yurtInformerFactory yurtinformers.SharedInformerFactory, workingMode util.WorkingMode, tenantNs string) { - // skip construct node/nodePool informers if service topology filter disabled - serviceTopologyFilterEnabled := isServiceTopologyFilterEnabled(options) - if serviceTopologyFilterEnabled { - if workingMode == util.WorkingModeCloud { - newNodeInformer := func(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - tweakListOptions := func(ops *metav1.ListOptions) { - ops.FieldSelector = fields.Set{"metadata.name": options.NodeName}.String() - } - return coreinformers.NewFilteredNodeInformer(client, resyncPeriod, nil, tweakListOptions) - } - informerFactory.InformerFor(&corev1.Node{}, newNodeInformer) - } - - if len(options.NodePoolName) != 0 { - newNodePoolInformer := func(client yurtclientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - tweakListOptions := func(ops *metav1.ListOptions) { - ops.FieldSelector = fields.Set{"metadata.name": options.NodePoolName}.String() - } - return yurtv1alpha1.NewFilteredNodePoolInformer(client, resyncPeriod, nil, tweakListOptions) - } - - yurtInformerFactory.InformerFor(&yurtcorev1alpha1.NodePool{}, newNodePoolInformer) - } - } - + // configmap informer is used by Yurthub filter approver newConfigmapInformer := func(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { tweakListOptions := func(options *metav1.ListOptions) { options.FieldSelector = fields.Set{"metadata.name": util.YurthubConfigMapName}.String() @@ -310,6 +277,7 @@ func registerInformers(options *options.YurtHubOptions, } informerFactory.InformerFor(&corev1.ConfigMap{}, newConfigmapInformer) + // secret informer is used by Tenant manager, this feature is not enabled in general. if tenantNs != "" { newSecretInformer := func(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return coreinformers.NewFilteredSecretInformer(client, tenantNs, resyncPeriod, nil, nil) @@ -317,6 +285,7 @@ func registerInformers(options *options.YurtHubOptions, informerFactory.InformerFor(&corev1.Secret{}, newSecretInformer) } + // pod informer is used by OTA updater on cloud working mode if workingMode == util.WorkingModeCloud { newPodInformer := func(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { listOptions := func(ops *metav1.ListOptions) { @@ -328,29 +297,6 @@ func registerInformers(options *options.YurtHubOptions, } } -// isServiceTopologyFilterEnabled is used to verify the service topology filter should be enabled or not. -func isServiceTopologyFilterEnabled(options *options.YurtHubOptions) bool { - if !options.EnableResourceFilter { - return false - } - - for _, filterName := range options.DisabledResourceFilters { - if filterName == filter.ServiceTopologyFilterName { - return false - } - } - - if options.WorkingMode == string(util.WorkingModeCloud) { - for i := range filter.DisabledInCloudMode { - if filter.DisabledInCloudMode[i] == filter.ServiceTopologyFilterName { - return false - } - } - } - - return true -} - func prepareServerServing(options *options.YurtHubOptions, certMgr certificate.YurtCertificateManager, cfg *YurtHubConfiguration) error { if err := (&apiserveroptions.DeprecatedInsecureServingOptions{ BindAddress: net.ParseIP(options.YurtHubHost), diff --git a/cmd/yurthub/app/start.go b/cmd/yurthub/app/start.go index ba6b166ede8..14e101a2245 100644 --- a/cmd/yurthub/app/start.go +++ b/cmd/yurthub/app/start.go @@ -176,7 +176,7 @@ func Run(ctx context.Context, cfg *config.YurtHubConfiguration) error { // Start the informer factory if all informers have been registered cfg.SharedFactory.Start(ctx.Done()) - cfg.YurtSharedFactory.Start(ctx.Done()) + cfg.NodePoolInformerFactory.Start(ctx.Done()) klog.Infof("%d. new reverse proxy handler for remote servers", trace) yurtProxyHandler, err := proxy.NewYurtReverseProxyHandler( diff --git a/go.mod b/go.mod index 53dcd2e2ad0..b26335a009b 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/onsi/ginkgo/v2 v2.1.4 github.com/onsi/gomega v1.19.0 github.com/opencontainers/selinux v1.11.0 - github.com/openyurtio/yurt-app-manager-api v0.6.0 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 github.com/projectcalico/api v0.0.0-20230222223746-44aa60c2201f diff --git a/go.sum b/go.sum index c77bd68f7d3..bf7f5a02e50 100644 --- a/go.sum +++ b/go.sum @@ -232,7 +232,6 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= @@ -381,7 +380,6 @@ github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -524,7 +522,6 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= @@ -532,7 +529,6 @@ github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= @@ -557,8 +553,6 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/openyurtio/apiserver-network-proxy v0.1.0 h1:uJI6LeAHmkQL0zV1+NIbgRsx2ayzsPfMA2bd1gROypc= github.com/openyurtio/apiserver-network-proxy v0.1.0/go.mod h1:X5Au3jBNIgYL2uK0IHeNGnZqlUlVSCFQhi/npPgkKRg= -github.com/openyurtio/yurt-app-manager-api v0.6.0 h1:GoayIUkdITBufJirU94dvyknFFG4On1T7XcDvsqCWaQ= -github.com/openyurtio/yurt-app-manager-api v0.6.0/go.mod h1:Ql/n89HmezW7s0d2Cyq9P3hl2MEvvjjv3xxPkLVzz10= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -1188,7 +1182,6 @@ k8s.io/csi-translation-lib v0.22.3/go.mod h1:YkdI+scWhZJQeA26iNg9XrKO3LhLz6dAcRK k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-aggregator v0.22.3/go.mod h1:TIpLq1HvR/S4y75i3y+4q9ik3ZvgyaDz72CBfDS0A6E= @@ -1208,7 +1201,6 @@ k8s.io/pod-security-admission v0.22.3/go.mod h1:xtkf/UhVWICokQLSDvD+8plfGkTQW4VT k8s.io/sample-apiserver v0.22.3/go.mod h1:HuEOdD/pT5R7gKNr2REb62uabZaJuFZyY3wUd86nFCA= k8s.io/system-validators v1.5.0/go.mod h1:bPldcLgkIUK22ALflnsXk8pvkTEndYdNuaHH6gRrl0Q= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210527160623-6fdb442a123b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs= @@ -1224,7 +1216,6 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22 h1:fmRfl9WJ4ApJn7LxNuED4m0t18qivVQOxP6aAYG9J6c= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/controller-runtime v0.9.0/go.mod h1:TgkfvrhhEw3PlI0BRL/5xM+89y3/yc0ZDfdbTl84si8= sigs.k8s.io/controller-runtime v0.10.3 h1:s5Ttmw/B4AuIbwrXD3sfBkXwnPMMWrqpVj4WRt1dano= sigs.k8s.io/controller-runtime v0.10.3/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= sigs.k8s.io/kustomize/api v0.8.11 h1:LzQzlq6Z023b+mBtc6v72N2mSHYmN8x7ssgbf/hv0H8= diff --git a/pkg/apis/apps/well_known_labels_annotations.go b/pkg/apis/apps/well_known_labels_annotations.go index a712a7b6676..7118efd71d4 100644 --- a/pkg/apis/apps/well_known_labels_annotations.go +++ b/pkg/apis/apps/well_known_labels_annotations.go @@ -40,6 +40,7 @@ const ( // LabelCurrentNodePool indicates which nodepool the node is currently // belonging to LabelCurrentNodePool = "apps.openyurt.io/nodepool" + NodePoolLabel = "apps.openyurt.io/nodepool" AnnotationPrevAttrs = "nodepool.openyurt.io/previous-attributes" diff --git a/pkg/util/kubeconfig/kubeconfig.go b/pkg/util/kubeconfig/kubeconfig.go index ca713622390..9625d2d3351 100644 --- a/pkg/util/kubeconfig/kubeconfig.go +++ b/pkg/util/kubeconfig/kubeconfig.go @@ -24,8 +24,6 @@ import ( clientset "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - - yurtclientset "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned" ) // CreateBasic creates a basic, general KubeConfig object that then can be extended @@ -127,18 +125,3 @@ func GetAuthInfoFromKubeConfig(config *clientcmdapi.Config) *clientcmdapi.AuthIn } return nil } - -// ToYurtClientSet converts a KubeConfig object to a yurtClient -func ToYurtClientSet(config *clientcmdapi.Config) (yurtclientset.Interface, error) { - overrides := clientcmd.ConfigOverrides{Timeout: "10s"} - clientConfig, err := clientcmd.NewDefaultClientConfig(*config, &overrides).ClientConfig() - if err != nil { - return nil, errors.Wrap(err, "failed to create yurt client configuration from kubeconfig") - } - - client, err := yurtclientset.NewForConfig(clientConfig) - if err != nil { - return nil, errors.Wrap(err, "failed to create yurt client") - } - return client, nil -} diff --git a/pkg/util/kubeconfig/kubeconfig_test.go b/pkg/util/kubeconfig/kubeconfig_test.go index 11d1a075e65..b1cce66144a 100644 --- a/pkg/util/kubeconfig/kubeconfig_test.go +++ b/pkg/util/kubeconfig/kubeconfig_test.go @@ -183,8 +183,6 @@ func TestWriteKubeconfigToDisk(t *testing.T) { newFile, ) } - client, err := ToYurtClientSet(c) - t.Log(client, err) }) } } diff --git a/pkg/util/kubernetes/kubeadm/app/util/apiclient/idempotency.go b/pkg/util/kubernetes/kubeadm/app/util/apiclient/idempotency.go index e8d85e2372e..4fecdde5bb7 100644 --- a/pkg/util/kubernetes/kubeadm/app/util/apiclient/idempotency.go +++ b/pkg/util/kubernetes/kubeadm/app/util/apiclient/idempotency.go @@ -24,13 +24,17 @@ import ( rbac "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientsetretry "k8s.io/client-go/util/retry" + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" "github.com/openyurtio/openyurt/pkg/util/kubernetes/kubeadm/app/constants" - nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" - yurtclientset "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned" ) // ConfigMapMutator is a function that mutates the given ConfigMap and optionally returns an error @@ -134,12 +138,24 @@ func GetConfigMapWithRetry(client clientset.Interface, namespace, name string) ( return nil, lastError } -func GetNodePoolInfoWithRetry(client yurtclientset.Interface, name string) (*nodepoolv1alpha1.NodePool, error) { - var np *nodepoolv1alpha1.NodePool +func GetNodePoolInfoWithRetry(cfg *clientcmdapi.Config, name string) (*v1beta1.NodePool, error) { + gvr := v1beta1.GroupVersion.WithResource("nodepools") + + clientConfig := clientcmd.NewDefaultClientConfig(*cfg, &clientcmd.ConfigOverrides{}) + restConfig, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + dynamicClient, err := dynamic.NewForConfig(restConfig) + if err != nil { + return nil, err + } + + var obj *unstructured.Unstructured var lastError error - err := wait.ExponentialBackoff(clientsetretry.DefaultBackoff, func() (bool, error) { + err = wait.ExponentialBackoff(clientsetretry.DefaultBackoff, func() (bool, error) { var err error - np, err = client.AppsV1alpha1().NodePools().Get(context.TODO(), name, metav1.GetOptions{}) + obj, err = dynamicClient.Resource(gvr).Get(context.TODO(), name, metav1.GetOptions{}) if err == nil { return true, nil } @@ -150,6 +166,10 @@ func GetNodePoolInfoWithRetry(client yurtclientset.Interface, name string) (*nod return false, nil }) if err == nil { + np := new(v1beta1.NodePool) + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), np); err != nil { + return nil, err + } return np, nil } return nil, lastError diff --git a/pkg/yurtadm/cmd/join/join.go b/pkg/yurtadm/cmd/join/join.go index ee81077a6be..1406c75b88d 100644 --- a/pkg/yurtadm/cmd/join/join.go +++ b/pkg/yurtadm/cmd/join/join.go @@ -30,6 +30,7 @@ import ( clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/klog/v2" + "github.com/openyurtio/openyurt/pkg/apis/apps" "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" kubeconfigutil "github.com/openyurtio/openyurt/pkg/util/kubeconfig" "github.com/openyurtio/openyurt/pkg/util/kubernetes/kubeadm/app/util/apiclient" @@ -39,7 +40,6 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" "github.com/openyurtio/openyurt/pkg/yurtadm/util/yurthub" - nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" ) type joinOptions struct { @@ -345,19 +345,13 @@ func newJoinData(args []string, opt *joinOptions) (*joinData, error) { // check whether specified nodePool exists if len(opt.nodePoolName) != 0 { - yurtClient, err := kubeconfigutil.ToYurtClientSet(cfg) - if err != nil { - klog.Errorf("failed to create yurt client, %v", err) - return nil, err - } - - np, err := apiclient.GetNodePoolInfoWithRetry(yurtClient, opt.nodePoolName) + np, err := apiclient.GetNodePoolInfoWithRetry(cfg, opt.nodePoolName) if err != nil || np == nil { // the specified nodePool not exist, return return nil, errors.Errorf("when --nodepool-name is specified, the specified nodePool should be exist.") } // add nodePool label for node by kubelet - data.nodeLabels[nodepoolv1alpha1.LabelDesiredNodePool] = opt.nodePoolName + data.nodeLabels[apps.NodePoolLabel] = opt.nodePoolName } // check static pods has value and yurtstaticset is already exist diff --git a/pkg/yurthub/filter/initializer/initializer.go b/pkg/yurthub/filter/initializer/initializer.go index 87d0d982185..6049372c4ad 100644 --- a/pkg/yurthub/filter/initializer/initializer.go +++ b/pkg/yurthub/filter/initializer/initializer.go @@ -17,11 +17,11 @@ limitations under the License. package initializer import ( + "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "github.com/openyurtio/openyurt/pkg/yurthub/filter" - yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" ) // WantsSharedInformerFactory is an interface for setting SharedInformerFactory @@ -29,9 +29,9 @@ type WantsSharedInformerFactory interface { SetSharedInformerFactory(factory informers.SharedInformerFactory) error } -// WantsYurtSharedInformerFactory is an interface for setting Yurt-App-Manager SharedInformerFactory -type WantsYurtSharedInformerFactory interface { - SetYurtSharedInformerFactory(yurtFactory yurtinformers.SharedInformerFactory) error +// WantsNodePoolInformerFactory is an interface for setting NodePool CRD SharedInformerFactory +type WantsNodePoolInformerFactory interface { + SetNodePoolInformerFactory(factory dynamicinformer.DynamicSharedInformerFactory) error } // WantsNodeName is an interface for setting node name @@ -58,7 +58,7 @@ type WantsKubeClient interface { // genericFilterInitializer is responsible for initializing generic filter type genericFilterInitializer struct { factory informers.SharedInformerFactory - yurtFactory yurtinformers.SharedInformerFactory + nodePoolFactory dynamicinformer.DynamicSharedInformerFactory nodeName string nodePoolName string masterServiceHost string @@ -68,12 +68,12 @@ type genericFilterInitializer struct { // New creates an filterInitializer object func New(factory informers.SharedInformerFactory, - yurtFactory yurtinformers.SharedInformerFactory, + nodePoolFactory dynamicinformer.DynamicSharedInformerFactory, kubeClient kubernetes.Interface, nodeName, nodePoolName, masterServiceHost, masterServicePort string) filter.Initializer { return &genericFilterInitializer{ factory: factory, - yurtFactory: yurtFactory, + nodePoolFactory: nodePoolFactory, nodeName: nodeName, nodePoolName: nodePoolName, masterServiceHost: masterServiceHost, @@ -112,8 +112,8 @@ func (fi *genericFilterInitializer) Initialize(ins filter.ObjectFilter) error { } } - if wants, ok := ins.(WantsYurtSharedInformerFactory); ok { - if err := wants.SetYurtSharedInformerFactory(fi.yurtFactory); err != nil { + if wants, ok := ins.(WantsNodePoolInformerFactory); ok { + if err := wants.SetNodePoolInformerFactory(fi.nodePoolFactory); err != nil { return err } } diff --git a/pkg/yurthub/filter/initializer/initializer_test.go b/pkg/yurthub/filter/initializer/initializer_test.go index efda901fb1d..74c1d26f69c 100644 --- a/pkg/yurthub/filter/initializer/initializer_test.go +++ b/pkg/yurthub/filter/initializer/initializer_test.go @@ -24,6 +24,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/dynamic/dynamicinformer" + dynamicfake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" @@ -33,21 +35,22 @@ import ( "github.com/openyurtio/openyurt/pkg/yurthub/filter/masterservice" "github.com/openyurtio/openyurt/pkg/yurthub/filter/nodeportisolation" "github.com/openyurtio/openyurt/pkg/yurthub/filter/servicetopology" - yurtfake "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned/fake" - yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" ) func TestNew(t *testing.T) { fakeClient := &fake.Clientset{} - fakeYurtClient := &yurtfake.Clientset{} sharedFactory := informers.NewSharedInformerFactory(fakeClient, 24*time.Hour) - yurtSharedFactory := yurtinformers.NewSharedInformerFactory(fakeYurtClient, 24*time.Hour) + + scheme := runtime.NewScheme() + fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(scheme) + nodePoolFactory := dynamicinformer.NewDynamicSharedInformerFactory(fakeDynamicClient, 24*time.Hour) + nodeName := "foo" nodePoolName := "foo-pool" masterServiceHost := "127.0.0.1" masterServicePort := "8080" - obj := New(sharedFactory, yurtSharedFactory, fakeClient, nodeName, nodePoolName, masterServiceHost, masterServicePort) + obj := New(sharedFactory, nodePoolFactory, fakeClient, nodeName, nodePoolName, masterServiceHost, masterServicePort) _, ok := obj.(filter.Initializer) if !ok { t.Errorf("expect a filter Initializer object, but got %v", reflect.TypeOf(obj)) @@ -85,15 +88,17 @@ func TestInitialize(t *testing.T) { }, } fakeClient := &fake.Clientset{} - fakeYurtClient := &yurtfake.Clientset{} sharedFactory := informers.NewSharedInformerFactory(fakeClient, 24*time.Hour) - yurtSharedFactory := yurtinformers.NewSharedInformerFactory(fakeYurtClient, 24*time.Hour) + + scheme := runtime.NewScheme() + fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(scheme) + nodePoolFactory := dynamicinformer.NewDynamicSharedInformerFactory(fakeDynamicClient, 24*time.Hour) nodeName := "foo" nodePoolName := "foo-pool" masterServiceHost := "127.0.0.1" masterServicePort := "8080" - obj := New(sharedFactory, yurtSharedFactory, fakeClient, nodeName, nodePoolName, masterServiceHost, masterServicePort) + obj := New(sharedFactory, nodePoolFactory, fakeClient, nodeName, nodePoolName, masterServiceHost, masterServicePort) for k, tc := range testcases { t.Run(k, func(t *testing.T) { diff --git a/pkg/yurthub/filter/manager/manager.go b/pkg/yurthub/filter/manager/manager.go index bc3d5f5dd91..b50dc3037b8 100644 --- a/pkg/yurthub/filter/manager/manager.go +++ b/pkg/yurthub/filter/manager/manager.go @@ -22,6 +22,7 @@ import ( "strconv" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" @@ -35,7 +36,6 @@ import ( "github.com/openyurtio/openyurt/pkg/yurthub/filter/servicetopology" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer" "github.com/openyurtio/openyurt/pkg/yurthub/util" - yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" ) type Manager struct { @@ -46,7 +46,7 @@ type Manager struct { func NewFilterManager(options *options.YurtHubOptions, sharedFactory informers.SharedInformerFactory, - yurtSharedFactory yurtinformers.SharedInformerFactory, + nodePoolFactory dynamicinformer.DynamicSharedInformerFactory, proxiedClient kubernetes.Interface, serializerManager *serializer.SerializerManager, apiserverAddr string) (*Manager, error) { @@ -70,7 +70,7 @@ func NewFilterManager(options *options.YurtHubOptions, } } - objFilters, err := createObjectFilters(filters, sharedFactory, yurtSharedFactory, proxiedClient, options.NodeName, options.NodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort) + objFilters, err := createObjectFilters(filters, sharedFactory, nodePoolFactory, proxiedClient, options.NodeName, options.NodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort) if err != nil { return nil, err } @@ -113,14 +113,14 @@ func (m *Manager) FindResponseFilter(req *http.Request) (filter.ResponseFilter, // createObjectFilters return all object filters that initializations completed. func createObjectFilters(filters *filter.Filters, sharedFactory informers.SharedInformerFactory, - yurtSharedFactory yurtinformers.SharedInformerFactory, + nodePoolFactory dynamicinformer.DynamicSharedInformerFactory, proxiedClient kubernetes.Interface, nodeName, nodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort string) ([]filter.ObjectFilter, error) { if filters == nil { return nil, nil } - genericInitializer := initializer.New(sharedFactory, yurtSharedFactory, proxiedClient, nodeName, nodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort) + genericInitializer := initializer.New(sharedFactory, nodePoolFactory, proxiedClient, nodeName, nodePoolName, mutatedMasterServiceHost, mutatedMasterServicePort) initializerChain := filter.Initializers{} initializerChain = append(initializerChain, genericInitializer) return filters.NewFromFilters(initializerChain) diff --git a/pkg/yurthub/filter/manager/manager_test.go b/pkg/yurthub/filter/manager/manager_test.go index e702b7cd1b9..9c4e6d31159 100644 --- a/pkg/yurthub/filter/manager/manager_test.go +++ b/pkg/yurthub/filter/manager/manager_test.go @@ -23,23 +23,27 @@ import ( "testing" "time" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/endpoints/filters" "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/dynamic/dynamicinformer" + dynamicfake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" "github.com/openyurtio/openyurt/cmd/yurthub/app/options" + "github.com/openyurtio/openyurt/pkg/apis" "github.com/openyurtio/openyurt/pkg/yurthub/filter" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/serializer" "github.com/openyurtio/openyurt/pkg/yurthub/proxy/util" - yurtfake "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned/fake" - yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" ) func TestFindResponseFilter(t *testing.T) { fakeClient := &fake.Clientset{} - fakeYurtClient := &yurtfake.Clientset{} + scheme := runtime.NewScheme() + apis.AddToScheme(scheme) + fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(scheme) serializerManager := serializer.NewSerializerManager() apiserverAddr := "127.0.0.1:6443" @@ -128,18 +132,19 @@ func TestFindResponseFilter(t *testing.T) { } options.DisabledResourceFilters = append(options.DisabledResourceFilters, tt.disabledResourceFilters...) - sharedFactory, yurtSharedFactory := informers.NewSharedInformerFactory(fakeClient, 24*time.Hour), - yurtinformers.NewSharedInformerFactory(fakeYurtClient, 24*time.Hour) + sharedFactory, nodePoolFactory := informers.NewSharedInformerFactory(fakeClient, 24*time.Hour), + dynamicinformer.NewDynamicSharedInformerFactory(fakeDynamicClient, 24*time.Hour) + stopper := make(chan struct{}) defer close(stopper) - mgr, _ := NewFilterManager(options, sharedFactory, yurtSharedFactory, fakeClient, serializerManager, apiserverAddr) + mgr, _ := NewFilterManager(options, sharedFactory, nodePoolFactory, fakeClient, serializerManager, apiserverAddr) if tt.mgrIsNil && mgr == nil { return } sharedFactory.Start(stopper) - yurtSharedFactory.Start(stopper) + nodePoolFactory.Start(stopper) req, err := http.NewRequest(tt.verb, tt.path, nil) if err != nil { diff --git a/pkg/yurthub/filter/nodeportisolation/filter.go b/pkg/yurthub/filter/nodeportisolation/filter.go index d21171f0f20..8ab3f428943 100644 --- a/pkg/yurthub/filter/nodeportisolation/filter.go +++ b/pkg/yurthub/filter/nodeportisolation/filter.go @@ -130,7 +130,7 @@ func (nif *nodePortIsolationFilter) resolveNodePoolName() string { klog.Warningf("skip isolateNodePortService filter, failed to get node(%s), %v", nif.nodeName, err) return nif.nodePoolName } - nif.nodePoolName = node.Labels[apps.LabelDesiredNodePool] + nif.nodePoolName = node.Labels[apps.NodePoolLabel] return nif.nodePoolName } diff --git a/pkg/yurthub/filter/servicetopology/filter.go b/pkg/yurthub/filter/servicetopology/filter.go index 209c1f47610..4e4cf7af76a 100644 --- a/pkg/yurthub/filter/servicetopology/filter.go +++ b/pkg/yurthub/filter/servicetopology/filter.go @@ -23,18 +23,19 @@ import ( discovery "k8s.io/api/discovery/v1" discoveryV1beta1 "k8s.io/api/discovery/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" listers "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" + "github.com/openyurtio/openyurt/pkg/apis/apps" + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" "github.com/openyurtio/openyurt/pkg/yurthub/filter" - nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" - yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" - appslisters "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/listers/apps/v1alpha1" ) const ( @@ -62,7 +63,7 @@ func NewServiceTopologyFilter() (filter.ObjectFilter, error) { type serviceTopologyFilter struct { serviceLister listers.ServiceLister serviceSynced cache.InformerSynced - nodePoolLister appslisters.NodePoolLister + nodePoolLister cache.GenericLister nodePoolSynced cache.InformerSynced nodePoolName string nodeName string @@ -87,9 +88,10 @@ func (stf *serviceTopologyFilter) SetSharedInformerFactory(factory informers.Sha return nil } -func (stf *serviceTopologyFilter) SetYurtSharedInformerFactory(yurtFactory yurtinformers.SharedInformerFactory) error { - stf.nodePoolLister = yurtFactory.Apps().V1alpha1().NodePools().Lister() - stf.nodePoolSynced = yurtFactory.Apps().V1alpha1().NodePools().Informer().HasSynced +func (stf *serviceTopologyFilter) SetNodePoolInformerFactory(dynamicInformerFactory dynamicinformer.DynamicSharedInformerFactory) error { + gvr := v1beta1.GroupVersion.WithResource("nodepools") + stf.nodePoolLister = dynamicInformerFactory.ForResource(gvr).Lister() + stf.nodePoolSynced = dynamicInformerFactory.ForResource(gvr).Informer().HasSynced return nil } @@ -120,7 +122,7 @@ func (stf *serviceTopologyFilter) resolveNodePoolName() string { klog.Warningf("failed to get node(%s) in serviceTopologyFilter filter, %v", stf.nodeName, err) return stf.nodePoolName } - stf.nodePoolName = node.Labels[nodepoolv1alpha1.LabelDesiredNodePool] + stf.nodePoolName = node.Labels[apps.NodePoolLabel] return stf.nodePoolName } @@ -228,11 +230,25 @@ func (stf *serviceTopologyFilter) nodePoolTopologyHandler(obj runtime.Object) ru return stf.nodeTopologyHandler(obj) } - nodePool, err := stf.nodePoolLister.Get(nodePoolName) + runtimeObj, err := stf.nodePoolLister.Get(nodePoolName) if err != nil { klog.Warningf("serviceTopologyFilterHandler: failed to get nodepool %s, err: %v", nodePoolName, err) return obj } + var nodePool *v1beta1.NodePool + switch poolObj := runtimeObj.(type) { + case *v1beta1.NodePool: + nodePool = poolObj + case *unstructured.Unstructured: + nodePool = new(v1beta1.NodePool) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(poolObj.UnstructuredContent(), nodePool); err != nil { + klog.Warningf("object(%#+v) is not a v1beta1.NodePool", poolObj) + return obj + } + default: + klog.Warningf("object(%#+v) is not a unknown type", poolObj) + return obj + } switch v := obj.(type) { case *discoveryV1beta1.EndpointSlice: @@ -247,7 +263,7 @@ func (stf *serviceTopologyFilter) nodePoolTopologyHandler(obj runtime.Object) ru } // reassembleV1beta1EndpointSlice will discard endpoints that are not on the same node/nodePool for v1beta1.EndpointSlice -func reassembleV1beta1EndpointSlice(endpointSlice *discoveryV1beta1.EndpointSlice, nodeName string, nodePool *nodepoolv1alpha1.NodePool) *discoveryV1beta1.EndpointSlice { +func reassembleV1beta1EndpointSlice(endpointSlice *discoveryV1beta1.EndpointSlice, nodeName string, nodePool *v1beta1.NodePool) *discoveryV1beta1.EndpointSlice { if len(nodeName) != 0 && nodePool != nil { klog.Warningf("reassembleV1beta1EndpointSlice: nodeName(%s) and nodePool can not be set at the same time", nodeName) return endpointSlice @@ -274,7 +290,7 @@ func reassembleV1beta1EndpointSlice(endpointSlice *discoveryV1beta1.EndpointSlic } // reassembleEndpointSlice will discard endpoints that are not on the same node/nodePool for v1.EndpointSlice -func reassembleEndpointSlice(endpointSlice *discovery.EndpointSlice, nodeName string, nodePool *nodepoolv1alpha1.NodePool) *discovery.EndpointSlice { +func reassembleEndpointSlice(endpointSlice *discovery.EndpointSlice, nodeName string, nodePool *v1beta1.NodePool) *discovery.EndpointSlice { if len(nodeName) != 0 && nodePool != nil { klog.Warningf("reassembleEndpointSlice: nodeName(%s) and nodePool can not be set at the same time", nodeName) return endpointSlice @@ -301,7 +317,7 @@ func reassembleEndpointSlice(endpointSlice *discovery.EndpointSlice, nodeName st } // reassembleEndpoints will discard subset that are not on the same node/nodePool for v1.Endpoints -func reassembleEndpoints(endpoints *v1.Endpoints, nodeName string, nodePool *nodepoolv1alpha1.NodePool) *v1.Endpoints { +func reassembleEndpoints(endpoints *v1.Endpoints, nodeName string, nodePool *v1beta1.NodePool) *v1.Endpoints { if len(nodeName) != 0 && nodePool != nil { klog.Warningf("reassembleEndpoints: nodeName(%s) and nodePool can not be set at the same time", nodeName) return endpoints @@ -329,7 +345,7 @@ func reassembleEndpoints(endpoints *v1.Endpoints, nodeName string, nodePool *nod return endpoints } -func filterValidEndpointsAddr(addresses []v1.EndpointAddress, nodeName string, nodePool *nodepoolv1alpha1.NodePool) []v1.EndpointAddress { +func filterValidEndpointsAddr(addresses []v1.EndpointAddress, nodeName string, nodePool *v1beta1.NodePool) []v1.EndpointAddress { var newEpAddresses []v1.EndpointAddress for i := range addresses { if addresses[i].NodeName == nil { diff --git a/pkg/yurthub/filter/servicetopology/filter_test.go b/pkg/yurthub/filter/servicetopology/filter_test.go index 3e234d92abf..01538a86b22 100644 --- a/pkg/yurthub/filter/servicetopology/filter_test.go +++ b/pkg/yurthub/filter/servicetopology/filter_test.go @@ -26,15 +26,18 @@ import ( discoveryV1beta1 "k8s.io/api/discovery/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/informers" k8sfake "k8s.io/client-go/kubernetes/fake" + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/apis/apps" + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" "github.com/openyurtio/openyurt/pkg/util" "github.com/openyurtio/openyurt/pkg/yurthub/filter" - nodepoolv1alpha1 "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/apis/apps/v1alpha1" - yurtfake "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/clientset/versioned/fake" - yurtinformers "github.com/openyurtio/yurt-app-manager-api/pkg/yurtappmanager/client/informers/externalversions" ) func TestName(t *testing.T) { @@ -63,6 +66,11 @@ func TestSupportedResourceAndVerbs(t *testing.T) { } func TestFilter(t *testing.T) { + scheme := runtime.NewScheme() + apis.AddToScheme(scheme) + gvrToListKind := map[schema.GroupVersionResource]string{ + {Group: "apps.openyurt.io", Version: "v1beta1", Resource: "nodepools"}: "NodePoolList", + } currentNodeName := "node1" nodeName2 := "node2" nodeName3 := "node3" @@ -72,7 +80,7 @@ func TestFilter(t *testing.T) { nodeName string responseObject runtime.Object kubeClient *k8sfake.Clientset - yurtClient *yurtfake.Clientset + yurtClient *fake.FakeDynamicClient expectObject runtime.Object }{ "v1beta1.EndpointSliceList: topologyKeys is kubernetes.io/hostname": { @@ -129,7 +137,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -137,7 +145,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -145,7 +153,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -159,29 +167,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -274,7 +282,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -282,7 +290,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -290,7 +298,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -304,29 +312,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -426,7 +434,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -434,7 +442,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -442,7 +450,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -456,29 +464,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -578,7 +586,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -586,7 +594,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -594,7 +602,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -606,29 +614,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -742,7 +750,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -750,7 +758,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -764,28 +772,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -877,7 +885,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -885,7 +893,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -893,7 +901,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -907,29 +915,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -1003,7 +1011,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1011,7 +1019,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1019,7 +1027,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1033,29 +1041,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, }, @@ -1126,7 +1134,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1134,7 +1142,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1142,7 +1150,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1156,29 +1164,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, }, @@ -1275,7 +1283,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1283,7 +1291,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1291,7 +1299,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1305,29 +1313,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -1407,7 +1415,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1415,7 +1423,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1423,7 +1431,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1437,29 +1445,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -1545,7 +1553,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1553,7 +1561,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1561,7 +1569,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1575,29 +1583,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -1683,7 +1691,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1691,7 +1699,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1699,7 +1707,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1711,29 +1719,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -1831,7 +1839,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1839,7 +1847,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1853,28 +1861,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -1942,7 +1950,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -1950,7 +1958,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1958,7 +1966,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -1972,28 +1980,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", "node3", @@ -2048,7 +2056,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2056,7 +2064,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2064,7 +2072,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2078,28 +2086,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", "node3", @@ -2163,7 +2171,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2171,7 +2179,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2179,7 +2187,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2193,29 +2201,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -2293,7 +2301,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2301,7 +2309,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2309,7 +2317,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2323,29 +2331,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -2427,7 +2435,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2435,7 +2443,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2443,7 +2451,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2457,29 +2465,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -2561,7 +2569,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2569,7 +2577,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2577,7 +2585,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2589,29 +2597,29 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -2703,7 +2711,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2711,7 +2719,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2725,28 +2733,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node3", }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", }, @@ -2816,7 +2824,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2824,7 +2832,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2832,7 +2840,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2846,28 +2854,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", "node3", @@ -2924,7 +2932,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -2932,7 +2940,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2940,7 +2948,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -2954,28 +2962,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", "node3", @@ -3032,7 +3040,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -3040,7 +3048,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -3048,7 +3056,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -3062,28 +3070,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", "node3", @@ -3132,7 +3140,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: currentNodeName, Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "hangzhou", + apps.NodePoolLabel: "hangzhou", }, }, }, @@ -3140,7 +3148,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node2", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -3148,7 +3156,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "node3", Labels: map[string]string{ - nodepoolv1alpha1.LabelDesiredNodePool: "shanghai", + apps.NodePoolLabel: "shanghai", }, }, }, @@ -3162,28 +3170,28 @@ func TestFilter(t *testing.T) { }, }, ), - yurtClient: yurtfake.NewSimpleClientset( - &nodepoolv1alpha1.NodePool{ + yurtClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "hangzhou", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ currentNodeName, }, }, }, - &nodepoolv1alpha1.NodePool{ + &v1beta1.NodePool{ ObjectMeta: metav1.ObjectMeta{ Name: "shanghai", }, - Spec: nodepoolv1alpha1.NodePoolSpec{ - Type: nodepoolv1alpha1.Edge, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, }, - Status: nodepoolv1alpha1.NodePoolStatus{ + Status: v1beta1.NodePoolStatus{ Nodes: []string{ "node2", "node3", @@ -3213,8 +3221,9 @@ func TestFilter(t *testing.T) { factory.Start(stopper) factory.WaitForCacheSync(stopper) - yurtFactory := yurtinformers.NewSharedInformerFactory(tt.yurtClient, 24*time.Hour) - nodePoolInformer := yurtFactory.Apps().V1alpha1().NodePools() + gvr := v1beta1.GroupVersion.WithResource("nodepools") + yurtFactory := dynamicinformer.NewDynamicSharedInformerFactory(tt.yurtClient, 24*time.Hour) + nodePoolInformer := yurtFactory.ForResource(gvr) nodePoolLister := nodePoolInformer.Lister() nodePoolSynced := nodePoolInformer.Informer().HasSynced From 9d0a63885f693f013a3c8be2024c7e52f35ed474 Mon Sep 17 00:00:00 2001 From: dsy3502 Date: Tue, 15 Aug 2023 10:59:23 +0800 Subject: [PATCH 67/93] replace str by const (#1645) --- pkg/yurttunnel/trafficforward/dns/dns.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/yurttunnel/trafficforward/dns/dns.go b/pkg/yurttunnel/trafficforward/dns/dns.go index e0a7970e37e..b9e833e4d89 100644 --- a/pkg/yurttunnel/trafficforward/dns/dns.go +++ b/pkg/yurttunnel/trafficforward/dns/dns.go @@ -53,7 +53,8 @@ const ( maxRetries = 15 minSyncPeriod = 30 - dnatPortPrefix = "dnat-" + dnatPortPrefix = "dnat-" + dnsControllerName = "tunnel-dns-controller" ) var ( @@ -141,7 +142,7 @@ func (dnsctl *coreDNSRecordController) Run(stopCh <-chan struct{}) { if err != nil { klog.Fatalf("failed to get hostname, %v", err) } - rl, err := resourcelock.New("leases", metav1.NamespaceSystem, "tunnel-dns-controller", + rl, err := resourcelock.New("leases", metav1.NamespaceSystem, dnsControllerName, dnsctl.kubeClient.CoreV1(), dnsctl.kubeClient.CoordinationV1(), resourcelock.ResourceLockConfig{ @@ -165,7 +166,7 @@ func (dnsctl *coreDNSRecordController) Run(stopCh <-chan struct{}) { }, }, WatchDog: electionChecker, - Name: "tunnel-dns-controller", + Name: dnsControllerName, }) panic("unreachable") } @@ -177,7 +178,7 @@ func (dnsctl *coreDNSRecordController) run(stopCh <-chan struct{}) { klog.Infof("starting tunnel dns controller") defer klog.Infof("shutting down tunnel dns controller") - if !cache.WaitForNamedCacheSync("tunnel-dns-controller", stopCh, + if !cache.WaitForNamedCacheSync(dnsControllerName, stopCh, dnsctl.nodeListerSynced, dnsctl.svcInformerSynced, dnsctl.cmInformerSynced) { return } From d837588c0e19fb8fdbbe60ff38d074cbc40226b3 Mon Sep 17 00:00:00 2001 From: Zhen Zhao <413621396@qq.com> Date: Tue, 15 Aug 2023 11:01:23 +0800 Subject: [PATCH 68/93] remove node-servant unused command (#1632) --- cmd/yurt-node-servant/node-servant.go | 4 +- .../preflight-convert/preflight.go | 69 --- pkg/node-servant/constant.go | 84 ---- pkg/node-servant/job.go | 10 - pkg/node-servant/preflight-convert/options.go | 126 ----- .../preflight-convert/preflight.go | 50 -- pkg/node-servant/preflight/checks.go | 436 ------------------ pkg/node-servant/preflight/constants.go | 27 -- pkg/node-servant/preflight/interface.go | 32 -- 9 files changed, 1 insertion(+), 837 deletions(-) delete mode 100644 cmd/yurt-node-servant/preflight-convert/preflight.go delete mode 100644 pkg/node-servant/preflight-convert/options.go delete mode 100644 pkg/node-servant/preflight-convert/preflight.go delete mode 100644 pkg/node-servant/preflight/checks.go delete mode 100644 pkg/node-servant/preflight/constants.go delete mode 100644 pkg/node-servant/preflight/interface.go diff --git a/cmd/yurt-node-servant/node-servant.go b/cmd/yurt-node-servant/node-servant.go index 001cb63ea59..3ffbdeed692 100644 --- a/cmd/yurt-node-servant/node-servant.go +++ b/cmd/yurt-node-servant/node-servant.go @@ -26,7 +26,6 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-node-servant/config" "github.com/openyurtio/openyurt/cmd/yurt-node-servant/convert" - preflightconvert "github.com/openyurtio/openyurt/cmd/yurt-node-servant/preflight-convert" "github.com/openyurtio/openyurt/cmd/yurt-node-servant/revert" upgrade "github.com/openyurtio/openyurt/cmd/yurt-node-servant/static-pod-upgrade" "github.com/openyurtio/openyurt/pkg/projectinfo" @@ -42,13 +41,12 @@ func main() { version := fmt.Sprintf("%#v", projectinfo.Get()) rootCmd := &cobra.Command{ Use: "node-servant", - Short: "node-servant do preflight-convert/convert/revert specific node", + Short: "node-servant do convert/revert specific node", Version: version, } rootCmd.PersistentFlags().String("kubeconfig", "", "The path to the kubeconfig file") rootCmd.AddCommand(convert.NewConvertCmd()) rootCmd.AddCommand(revert.NewRevertCmd()) - rootCmd.AddCommand(preflightconvert.NewxPreflightConvertCmd()) rootCmd.AddCommand(config.NewConfigCmd()) rootCmd.AddCommand(upgrade.NewUpgradeCmd()) diff --git a/cmd/yurt-node-servant/preflight-convert/preflight.go b/cmd/yurt-node-servant/preflight-convert/preflight.go deleted file mode 100644 index 1db8c0dcec7..00000000000 --- a/cmd/yurt-node-servant/preflight-convert/preflight.go +++ /dev/null @@ -1,69 +0,0 @@ -/* -Copyright 2021 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package preflight_convert - -import ( - "os" - - "github.com/spf13/cobra" - "k8s.io/klog/v2" - - preflightconvert "github.com/openyurtio/openyurt/pkg/node-servant/preflight-convert" -) - -const ( - latestYurtHubImage = "openyurt/yurthub:latest" - latestYurtTunnelAgentImage = "openyurt/yurt-tunnel-agent:latest" -) - -// NewxPreflightConvertCmd generates a new preflight-convert check command -func NewxPreflightConvertCmd() *cobra.Command { - o := preflightconvert.NewPreflightConvertOptions() - cmd := &cobra.Command{ - Use: "preflight-convert", - Short: "", - Run: func(cmd *cobra.Command, args []string) { - if err := o.Complete(cmd.Flags()); err != nil { - klog.Errorf("Fail to complete the preflight-convert option: %s", err) - os.Exit(1) - } - preflighter := preflightconvert.NewPreflighterWithOptions(o) - if err := preflighter.Do(); err != nil { - klog.Errorf("Fail to run pre-flight checks: %s", err) - os.Exit(1) - } - klog.Info("convert pre-flight checks success") - }, - Args: cobra.NoArgs, - } - setFlags(cmd) - - return cmd -} - -func setFlags(cmd *cobra.Command) { - cmd.Flags().StringP("kubeadm-conf-path", "k", "", - "The path to kubelet service conf that is used by kubelet component to join the cluster on the work node."+ - "Support multiple values, will search in order until get the file.(e.g -k kbcfg1,kbcfg2)", - ) - cmd.Flags().String("yurthub-image", latestYurtHubImage, "The yurthub image.") - cmd.Flags().String("yurt-tunnel-agent-image", latestYurtTunnelAgentImage, "The yurt-tunnel-agent image.") - cmd.Flags().BoolP("deploy-yurttunnel", "t", false, "If set, yurt-tunnel-agent will be deployed.") - cmd.Flags().String("ignore-preflight-errors", "", "A list of checks whose errors will be shown as warnings. "+ - "Example: 'isprivilegeduser,imagepull'.Value 'all' ignores errors from all checks.", - ) -} diff --git a/pkg/node-servant/constant.go b/pkg/node-servant/constant.go index 5b268e9cfbc..b91d0b4ed0b 100644 --- a/pkg/node-servant/constant.go +++ b/pkg/node-servant/constant.go @@ -23,12 +23,6 @@ const ( // RevertJobNameBase is the prefix of the revert ServantJob name RevertJobNameBase = "node-servant-revert" - //ConvertPreflightJobNameBase is the prefix of the preflight-convert ServantJob name - ConvertPreflightJobNameBase = "node-servant-preflight-convert" - - // ConfigControlPlaneJobNameBase is the prefix of the config control-plane ServantJob name - ConfigControlPlaneJobNameBase = "config-control-plane" - // ConvertServantJobTemplate defines the node convert servant job in yaml format ConvertServantJobTemplate = ` apiVersion: batch/v1 @@ -114,83 +108,5 @@ spec: - name: KUBELET_SVC value: {{.kubeadm_conf_path}} {{end}} -` - // ConvertPreflightJobTemplate defines the node convert preflight checks servant job in yaml format - ConvertPreflightJobTemplate = ` -apiVersion: batch/v1 -kind: Job -metadata: - name: {{.jobName}} - namespace: kube-system -spec: - template: - spec: - hostPID: true - hostNetwork: true - restartPolicy: OnFailure - nodeName: {{.nodeName}} - volumes: - - name: host-root - hostPath: - path: / - type: Directory - containers: - - name: node-servant - image: {{.node_servant_image}} - imagePullPolicy: IfNotPresent - command: - - /bin/sh - - -c - args: - - "/usr/local/bin/entry.sh preflight-convert {{if .ignore_preflight_errors}}--ignore-preflight-errors {{.ignore_preflight_errors}} {{end}}" - securityContext: - privileged: true - volumeMounts: - - mountPath: /openyurt - name: host-root - env: - - name: NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - {{if .kubeadm_conf_path }} - - name: KUBELET_SVC - value: {{.kubeadm_conf_path}} - {{end}} -` - - // ConfigControlPlaneJobTemplate defines the node-servant config control-plane for configuring kube-apiserver and kube-controller-manager - ConfigControlPlaneJobTemplate = ` -apiVersion: batch/v1 -kind: Job -metadata: - name: {{.jobName}} - namespace: kube-system -spec: - template: - spec: - hostPID: true - hostNetwork: true - restartPolicy: OnFailure - nodeName: {{.nodeName}} - volumes: - - name: host-root - hostPath: - path: / - type: Directory - containers: - - name: node-servant - image: {{.node_servant_image}} - imagePullPolicy: IfNotPresent - command: - - /bin/sh - - -c - args: - - "/usr/local/bin/entry.sh config control-plane" - securityContext: - privileged: true - volumeMounts: - - mountPath: /openyurt - name: host-root ` ) diff --git a/pkg/node-servant/job.go b/pkg/node-servant/job.go index 36f44034da7..b14d86fac80 100644 --- a/pkg/node-servant/job.go +++ b/pkg/node-servant/job.go @@ -46,12 +46,6 @@ func RenderNodeServantJob(action string, renderCtx map[string]string, nodeName s case "revert": servantJobTemplate = RevertServantJobTemplate jobBaseName = RevertJobNameBase - case "preflight-convert": - servantJobTemplate = ConvertPreflightJobTemplate - jobBaseName = ConvertPreflightJobNameBase - case "config-control-plane": - servantJobTemplate = ConfigControlPlaneJobTemplate - jobBaseName = ConfigControlPlaneJobNameBase } tmplCtx["jobName"] = jobBaseName + "-" + nodeName @@ -95,10 +89,6 @@ func validate(action string, tmplCtx map[string]string, nodeName string) error { case "revert": keysMustHave := []string{"node_servant_image"} return checkKeys(keysMustHave, tmplCtx) - case "preflight-convert", "config-control-plane": - keysMustHave := []string{"node_servant_image"} - return checkKeys(keysMustHave, tmplCtx) - default: return fmt.Errorf("action invalied: %s ", action) } diff --git a/pkg/node-servant/preflight-convert/options.go b/pkg/node-servant/preflight-convert/options.go deleted file mode 100644 index 8c02e2df5da..00000000000 --- a/pkg/node-servant/preflight-convert/options.go +++ /dev/null @@ -1,126 +0,0 @@ -/* -Copyright 2021 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package preflight_convert - -import ( - "strings" - - "github.com/spf13/pflag" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/sets" - - "github.com/openyurtio/openyurt/pkg/node-servant/components" -) - -const ( - kubeAdmFlagsEnvFile = "/var/lib/kubelet/kubeadm-flags.env" -) - -// Options has the information that required by preflight-convert operation -type Options struct { - KubeadmConfPaths []string - YurthubImage string - YurttunnelAgentImage string - DeployTunnel bool - IgnorePreflightErrors sets.String - - KubeAdmFlagsEnvFile string - ImagePullPolicy v1.PullPolicy - CRISocket string -} - -func (o *Options) GetCRISocket() string { - return o.CRISocket -} - -func (o *Options) GetImageList() []string { - imgs := []string{} - - imgs = append(imgs, o.YurthubImage) - if o.DeployTunnel { - imgs = append(imgs, o.YurttunnelAgentImage) - } - return imgs -} - -func (o *Options) GetImagePullPolicy() v1.PullPolicy { - return o.ImagePullPolicy -} - -func (o *Options) GetKubeadmConfPaths() []string { - return o.KubeadmConfPaths -} - -func (o *Options) GetKubeAdmFlagsEnvFile() string { - return o.KubeAdmFlagsEnvFile -} - -// NewPreflightConvertOptions creates a new Options -func NewPreflightConvertOptions() *Options { - return &Options{ - KubeadmConfPaths: components.GetDefaultKubeadmConfPath(), - IgnorePreflightErrors: sets.NewString(), - KubeAdmFlagsEnvFile: kubeAdmFlagsEnvFile, - ImagePullPolicy: v1.PullIfNotPresent, - } -} - -// Complete completes all the required options. -func (o *Options) Complete(flags *pflag.FlagSet) error { - - kubeadmConfPaths, err := flags.GetString("kubeadm-conf-path") - if err != nil { - return err - } - if kubeadmConfPaths != "" { - o.KubeadmConfPaths = strings.Split(kubeadmConfPaths, ",") - } - - yurthubImage, err := flags.GetString("yurthub-image") - if err != nil { - return err - } - o.YurthubImage = yurthubImage - - yurttunnelAgentImage, err := flags.GetString("yurt-tunnel-agent-image") - if err != nil { - return err - } - o.YurttunnelAgentImage = yurttunnelAgentImage - - dt, err := flags.GetBool("deploy-yurttunnel") - if err != nil { - return err - } - o.DeployTunnel = dt - - ipStr, err := flags.GetString("ignore-preflight-errors") - if err != nil { - return err - } - if ipStr != "" { - ipStr = strings.ToLower(ipStr) - o.IgnorePreflightErrors = sets.NewString(strings.Split(ipStr, ",")...) - } - - CRISocket, err := components.DetectCRISocket() - if err != nil { - return err - } - o.CRISocket = CRISocket - return nil -} diff --git a/pkg/node-servant/preflight-convert/preflight.go b/pkg/node-servant/preflight-convert/preflight.go deleted file mode 100644 index d404e40fc5e..00000000000 --- a/pkg/node-servant/preflight-convert/preflight.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2021 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package preflight_convert - -import ( - "k8s.io/klog/v2" - - "github.com/openyurtio/openyurt/pkg/node-servant/preflight" -) - -// ConvertPreflighter do the preflight-convert-convert job -type ConvertPreflighter struct { - Options -} - -// NewPreflighterWithOptions create nodePreflighter -func NewPreflighterWithOptions(o *Options) *ConvertPreflighter { - return &ConvertPreflighter{ - *o, - } -} - -func (n *ConvertPreflighter) Do() error { - klog.Infof("[preflight-convert] Running node-servant pre-flight checks") - if err := preflight.RunConvertNodeChecks(n, n.IgnorePreflightErrors, n.DeployTunnel); err != nil { - return err - } - - klog.Infof("[preflight-convert] Pulling images required for converting a Kubernetes cluster to an OpenYurt cluster") - klog.Infof("[preflight-convert] This might take a minute or two, depending on the speed of your internet connection") - if err := preflight.RunPullImagesCheck(n, n.IgnorePreflightErrors); err != nil { - return err - } - - return nil -} diff --git a/pkg/node-servant/preflight/checks.go b/pkg/node-servant/preflight/checks.go deleted file mode 100644 index 22eb1fe5cec..00000000000 --- a/pkg/node-servant/preflight/checks.go +++ /dev/null @@ -1,436 +0,0 @@ -/* -Copyright 2021 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package preflight - -import ( - "bytes" - "fmt" - "io" - "net" - "os" - "strings" - "sync" - "time" - - "github.com/pkg/errors" - batchv1 "k8s.io/api/batch/v1" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/kubernetes" - "k8s.io/klog/v2" - utilsexec "k8s.io/utils/exec" - - nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" - "github.com/openyurtio/openyurt/pkg/node-servant/components" - "github.com/openyurtio/openyurt/pkg/projectinfo" - kubeutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" -) - -// Error defines struct for communicating error messages generated by preflight-convert-convert checks -type Error struct { - Msg string -} - -// Error implements the standard error interface -func (e *Error) Error() string { - return fmt.Sprintf("[preflight] Some fatal errors occurred:\n%s%s", e.Msg, "[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`\n") -} - -// Preflight identifies this error as a preflight-convert-convert error -func (e *Error) Preflight() bool { - return true -} - -// Checker validates the state of the system to ensure kubeadm will be -// successful as often as possible. -type Checker interface { - Check() (warnings, errorList []error) - Name() string -} - -// IsPrivilegedUserCheck verifies user is privileged (linux - root). -// The check under windows environment has not been implemented yet. -type IsPrivilegedUserCheck struct{} - -func (IsPrivilegedUserCheck) Name() string { - return "IsPrivilegedUser" -} - -// Check validates if an user has elevated (root) privileges. -func (ipuc IsPrivilegedUserCheck) Check() (warnings, errorList []error) { - if os.Getuid() != 0 { - return nil, []error{errors.New("user is not running as root")} - } - - return nil, nil -} - -// NodeReadyCheck checks the nodes status whether is ready. -type NodeReadyCheck struct { - NodeLst *v1.NodeList -} - -func (NodeReadyCheck) Name() string { - return "NodeReady" -} - -func (nrc NodeReadyCheck) Check() (warnings, errorList []error) { - klog.V(1).Infoln("validating node status") - var notReadyNodeNames []string - for _, node := range nrc.NodeLst.Items { - if !isNodeReady(&node.Status) { - notReadyNodeNames = append(notReadyNodeNames, node.Name) - } - } - if len(notReadyNodeNames) != 0 { - return nil, []error{errors.Errorf("the status of nodes: %s is not 'Ready'", notReadyNodeNames)} - } - return nil, nil -} - -// NodeEdgeWorkerLabelCheck checks whether the node contains edgeWorker label which represents an OpenYurt node. -type NodeEdgeWorkerLabelCheck struct { - NodeLst *v1.NodeList -} - -func (NodeEdgeWorkerLabelCheck) Name() string { - return "NodeEdgeWorkerLabel" -} - -func (nlc NodeEdgeWorkerLabelCheck) Check() (warnings, errorList []error) { - klog.V(1).Infoln("validating node edgeworker label") - var hasLabelNodeNames []string - for _, node := range nlc.NodeLst.Items { - if _, ok := node.Labels[projectinfo.GetEdgeWorkerLabelKey()]; ok { - hasLabelNodeNames = append(hasLabelNodeNames, node.Name) - } - } - if len(hasLabelNodeNames) != 0 { - return nil, []error{errors.Errorf("the nodes %s has already been labeled as a OpenYurt node", hasLabelNodeNames)} - } - return nil, nil -} - -// JobExistCheck checks whether the jobs with a specific prefix exist. -type JobExistCheck struct { - JobLst *batchv1.JobList - Prefix string - Label string -} - -func (jc JobExistCheck) Name() string { - if jc.Label != "" { - return jc.Label - } - return fmt.Sprintf("JobExist-%s", jc.Prefix) -} - -func (jc JobExistCheck) Check() (warnings, errorList []error) { - klog.V(1).Infoln("validating convert jobs") - var invalidJobNames []string - for _, job := range jc.JobLst.Items { - if strings.HasPrefix(job.Name, jc.Prefix) { - invalidJobNames = append(invalidJobNames, job.Name) - } - } - if len(invalidJobNames) != 0 { - return nil, []error{errors.Errorf("jobs %s has prefix %s, may conflict with the conversion job name", invalidJobNames, jc.Prefix)} - } - return nil, nil -} - -// NodeServantJobCheck create jobs to do preflight checks. -// After the job is successfully executed, it will be deleted. -// The failed job will not be deleted, and the user needs to delete it manually. -type NodeServantJobCheck struct { - cliSet *kubernetes.Clientset - jobLst []*batchv1.Job - - waitServantJobTimeout time.Duration - checkServantJobPeriod time.Duration -} - -func (NodeServantJobCheck) Name() string { - return "NodeServantJob" -} - -func (nc NodeServantJobCheck) Check() (warnings []error, errorList []error) { - var wg sync.WaitGroup - - res := make(chan error, len(nc.jobLst)) - for _, job := range nc.jobLst { - wg.Add(1) - entity := *job - go func() { - defer wg.Done() - if err := kubeutil.RunJobAndCleanup(nc.cliSet, &entity, - nc.waitServantJobTimeout, nc.checkServantJobPeriod, false); err != nil { - msg := fmt.Errorf("fail to run servant job(%s): %w\n", entity.GetName(), err) - res <- msg - } else { - klog.V(1).Infof("servant job(%s) has succeeded\n", entity.GetName()) - } - }() - } - wg.Wait() - close(res) - for m := range res { - errorList = append(errorList, m) - } - return nil, errorList -} - -// FileExistingCheck checks that the given file already exist. -type FileExistingCheck struct { - Path string - Label string -} - -// Name returns label for individual FileExistingChecks. If not known, will return based on path. -func (fac FileExistingCheck) Name() string { - if fac.Label != "" { - return fac.Label - } - return fmt.Sprintf("FileExisting-%s", strings.Replace(fac.Path, "/", "-", -1)) -} - -func (fac FileExistingCheck) Check() (warnings, errorList []error) { - klog.V(1).Infof("validating the existence of file %s", fac.Path) - - if _, err := os.Stat(fac.Path); err != nil { - return nil, []error{errors.Errorf("%s doesn't exist", fac.Path)} - } - return nil, nil -} - -// FileAtLeastOneExistingCheck checks if at least one file exists in the file list. -// After a file is found, the remaining files will not be checked. -type FileAtLeastOneExistingCheck struct { - Paths []string - Label string -} - -func (foc FileAtLeastOneExistingCheck) Name() string { - if foc.Label != "" { - return foc.Label - } - return fmt.Sprintf("FileAtLeastOneExistingCheck-%s", foc.Paths[0]) -} - -func (foc FileAtLeastOneExistingCheck) Check() (warnings, errorList []error) { - klog.V(1).Infof("validating if at least one file exists in the file list: %s", foc.Paths) - for _, path := range foc.Paths { - if _, err := os.Stat(path); err == nil { - return nil, nil - } - } - return nil, []error{errors.Errorf("no file in list %s exists", foc.Paths)} - -} - -// DirExistingCheck checks if the given directory either exist, or is not empty. -type DirExistingCheck struct { - Path string - Label string -} - -// Name returns label for individual DirExistingChecks. If not known, will return based on path. -func (dac DirExistingCheck) Name() string { - if dac.Label != "" { - return dac.Label - } - return fmt.Sprintf("DirExisting-%s", strings.Replace(dac.Path, "/", "-", -1)) -} - -// Check validates if a directory exists or does not empty. -func (dac DirExistingCheck) Check() (warnings, errorList []error) { - klog.V(1).Infof("validating the existence of directory %s", dac.Path) - - if _, err := os.Stat(dac.Path); os.IsNotExist(err) { - return nil, []error{errors.Errorf("%s doesn't exist", dac.Path)} - } - - f, err := os.Open(dac.Path) - if err != nil { - return nil, []error{errors.Wrapf(err, "unable to check if %s is empty", dac.Path)} - } - defer f.Close() - - _, err = f.Readdirnames(1) - if err == io.EOF { - return nil, []error{errors.Errorf("%s is empty", dac.Path)} - } - return nil, nil -} - -// PortOpenCheck ensures the given port is available for use. -type PortOpenCheck struct { - port int - label string -} - -func (poc PortOpenCheck) Name() string { - if poc.label != "" { - return poc.label - } - return fmt.Sprintf("Port-%d", poc.port) -} - -func (poc PortOpenCheck) Check() (warnings, errorList []error) { - klog.V(1).Infof("validating availability of port %d", poc.port) - - ln, err := net.Listen("tcp", fmt.Sprintf(":%d", poc.port)) - if err != nil { - errorList = []error{errors.Errorf("Port %d is in use", poc.port)} - } - if ln != nil { - if err = ln.Close(); err != nil { - warnings = append(warnings, errors.Errorf("when closing port %d, encountered %v", poc.port, err)) - } - } - return warnings, errorList -} - -// ImagePullCheck will pull container images used by node-servant -type ImagePullCheck struct { - //runtime utilruntime.ContainerRuntime - runtime components.ContainerRuntimeForImage - imageList []string - imagePullPolicy v1.PullPolicy -} - -func (ImagePullCheck) Name() string { - return "ImagePull" -} - -func (ipc ImagePullCheck) Check() (warnings, errorList []error) { - policy := ipc.imagePullPolicy - klog.V(1).Infof("using image pull policy: %s", policy) - for _, image := range ipc.imageList { - switch policy { - case v1.PullNever: - klog.V(1).Infof("skipping pull of image: %s", image) - continue - case v1.PullIfNotPresent: - ret, err := ipc.runtime.ImageExists(image) - if ret && err == nil { - klog.V(1).Infof("image exists: %s", image) - continue - } - if err != nil { - errorList = append(errorList, errors.Wrapf(err, "failed to check if image %s exists", image)) - } - fallthrough // Proceed with pulling the image if it does not exist - case v1.PullAlways: - klog.V(1).Infof("pulling: %s", image) - if err := ipc.runtime.PullImage(image); err != nil { - errorList = append(errorList, errors.Wrapf(err, "failed to pull image %s", image)) - } - default: - // If the policy is unknown return early with an error - errorList = append(errorList, errors.Errorf("unsupported pull policy %q", policy)) - return warnings, errorList - } - } - return warnings, errorList -} - -func RunConvertNodeChecks(o KubePathOperator, ignorePreflightErrors sets.String, deployTunnel bool) error { - // First, check if we're root separately from the other preflight-convert-convert checks and fail fast - if err := RunRootCheckOnly(ignorePreflightErrors); err != nil { - return err - } - - checks := []Checker{ - FileAtLeastOneExistingCheck{Paths: o.GetKubeadmConfPaths(), Label: "KubeadmConfig"}, - FileExistingCheck{Path: o.GetKubeAdmFlagsEnvFile(), Label: "KubeAdmFlagsEnv"}, - DirExistingCheck{Path: KubernetesDir}, - DirExistingCheck{Path: KubeletPkiDir}, - PortOpenCheck{port: YurtHubProxySecurePort}, - PortOpenCheck{port: YurtHubProxyPort}, - PortOpenCheck{port: YurtHubPort}, - } - - if deployTunnel { - checks = append(checks, PortOpenCheck{port: YurttunnelAgentPort}) - } - return RunChecks(checks, os.Stderr, ignorePreflightErrors) - -} - -// RunRootCheckOnly initializes checks slice of structs and call RunChecks -func RunRootCheckOnly(ignorePreflightErrors sets.String) error { - checks := []Checker{ - IsPrivilegedUserCheck{}, - } - - return RunChecks(checks, os.Stderr, ignorePreflightErrors) -} - -// RunPullImagesCheck will pull images convert needs if they are not found on the system -func RunPullImagesCheck(o ImageOperator, ignorePreflightErrors sets.String) error { - containerRuntime, err := components.NewContainerRuntimeForImage(utilsexec.New(), o.GetCRISocket()) - if err != nil { - return err - } - - checks := []Checker{ - ImagePullCheck{runtime: containerRuntime, imageList: o.GetImageList(), imagePullPolicy: o.GetImagePullPolicy()}, - } - return RunChecks(checks, os.Stderr, ignorePreflightErrors) -} - -// RunChecks runs each check, displays it's warnings/errors, and once all -// are processed will exit if any errors occurred. -func RunChecks(checks []Checker, ww io.Writer, ignorePreflightErrors sets.String) error { - var errsBuffer bytes.Buffer - - for _, c := range checks { - name := c.Name() - warnings, errs := c.Check() - - if setHasItemOrAll(ignorePreflightErrors, name) { - // Decrease severity of errors to warnings for this check - warnings = append(warnings, errs...) - errs = []error{} - } - - for _, w := range warnings { - io.WriteString(ww, fmt.Sprintf("\t[WARNING %s]: %v\n", name, w)) - } - for _, i := range errs { - errsBuffer.WriteString(fmt.Sprintf("\t[ERROR %s]: %v\n", name, i.Error())) - } - } - if errsBuffer.Len() > 0 { - return &Error{Msg: errsBuffer.String()} - } - return nil -} - -// setHasItemOrAll is helper function that return true if item is present in the set (case insensitive) or special key 'all' is present -func setHasItemOrAll(s sets.String, item string) bool { - if s.Has("all") || s.Has(strings.ToLower(item)) { - return true - } - return false -} - -func isNodeReady(status *v1.NodeStatus) bool { - _, condition := nodeutil.GetNodeCondition(status, v1.NodeReady) - return condition != nil && condition.Status == v1.ConditionTrue -} diff --git a/pkg/node-servant/preflight/constants.go b/pkg/node-servant/preflight/constants.go deleted file mode 100644 index 7b3c2cf8299..00000000000 --- a/pkg/node-servant/preflight/constants.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2021 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package preflight - -const ( - KubernetesDir = "/etc/kubernetes" - KubeletPkiDir = "/var/lib/kubelet/pki" - - YurtHubProxySecurePort = 10268 - YurtHubProxyPort = 10261 - YurtHubPort = 10267 - YurttunnelAgentPort = 10266 -) diff --git a/pkg/node-servant/preflight/interface.go b/pkg/node-servant/preflight/interface.go deleted file mode 100644 index 9581449a7fe..00000000000 --- a/pkg/node-servant/preflight/interface.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2021 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package preflight - -import ( - v1 "k8s.io/api/core/v1" -) - -type ImageOperator interface { - GetCRISocket() string - GetImageList() []string - GetImagePullPolicy() v1.PullPolicy -} - -type KubePathOperator interface { - GetKubeadmConfPaths() []string - GetKubeAdmFlagsEnvFile() string -} From 8c91be5f797db1394931223c4856f71afbbcc725 Mon Sep 17 00:00:00 2001 From: dsy3502 Date: Tue, 15 Aug 2023 11:16:23 +0800 Subject: [PATCH 69/93] replace string var by const (#1635) --- .../yurtappdaemon/v1alpha1/yurtappdaemon_validation.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go b/pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go index 991eef78a7e..fca4cd2ed70 100644 --- a/pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go +++ b/pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go @@ -39,6 +39,10 @@ import ( "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) +const ( + YurtAppDaemonKind = "YurtAppDaemon" +) + // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. func (webhook *YurtAppDaemonHandler) ValidateCreate(ctx context.Context, obj runtime.Object) error { daemon, ok := obj.(*v1alpha1.YurtAppDaemon) @@ -47,7 +51,7 @@ func (webhook *YurtAppDaemonHandler) ValidateCreate(ctx context.Context, obj run } if allErrs := validateYurtAppDaemon(webhook.Client, daemon); len(allErrs) > 0 { - return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind("YurtAppDaemon").GroupKind(), daemon.Name, allErrs) + return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind(YurtAppDaemonKind).GroupKind(), daemon.Name, allErrs) } return nil @@ -67,7 +71,7 @@ func (webhook *YurtAppDaemonHandler) ValidateUpdate(ctx context.Context, oldObj, validationErrorList := validateYurtAppDaemon(webhook.Client, newDaemon) updateErrorList := ValidateYurtAppDaemonUpdate(newDaemon, oldDaemon) if allErrs := append(validationErrorList, updateErrorList...); len(allErrs) > 0 { - return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind("YurtAppDaemon").GroupKind(), newDaemon.Name, allErrs) + return apierrors.NewInvalid(v1alpha1.GroupVersion.WithKind(YurtAppDaemonKind).GroupKind(), newDaemon.Name, allErrs) } return nil } From 01a49853612176f2abf0481cfeed52900d218d8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 11:46:23 +0800 Subject: [PATCH 70/93] build(deps): bump google.golang.org/grpc from 1.56.2 to 1.57.0 (#1634) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.56.2 to 1.57.0. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.56.2...v1.57.0) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 ++++-- go.sum | 12 ++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b26335a009b..39e28a77e40 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( go.etcd.io/etcd/client/v3 v3.5.0 golang.org/x/net v0.9.0 golang.org/x/sys v0.10.0 - google.golang.org/grpc v1.56.2 + google.golang.org/grpc v1.57.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.1 @@ -155,7 +155,9 @@ require ( golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.2 // indirect diff --git a/go.sum b/go.sum index bf7f5a02e50..40c5893a6c5 100644 --- a/go.sum +++ b/go.sum @@ -1070,8 +1070,12 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1087,8 +1091,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= -google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From f4c8d287c8a91873e0e053c4109df9b845408077 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 12:52:23 +0800 Subject: [PATCH 71/93] build(deps): bump golang.org/x/net from 0.9.0 to 0.14.0 (#1643) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.9.0 to 0.14.0. - [Commits](https://github.com/golang/net/compare/v0.9.0...v0.14.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 39e28a77e40..7671b2da1fa 100644 --- a/go.mod +++ b/go.mod @@ -26,8 +26,8 @@ require ( go.etcd.io/etcd/api/v3 v3.5.0 go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 - golang.org/x/net v0.9.0 - golang.org/x/sys v0.10.0 + golang.org/x/net v0.14.0 + golang.org/x/sys v0.11.0 google.golang.org/grpc v1.57.0 gopkg.in/cheggaaa/pb.v1 v1.0.28 gopkg.in/square/go-jose.v2 v2.6.0 @@ -147,11 +147,11 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.5.0 // indirect + golang.org/x/crypto v0.12.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/term v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/term v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 40c5893a6c5..b46474bda68 100644 --- a/go.sum +++ b/go.sum @@ -776,8 +776,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -858,8 +858,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -944,13 +944,13 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -960,8 +960,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From dbd2eaaf4ab6c35707b9dd43be76e01ebeaeb7e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 12:54:23 +0800 Subject: [PATCH 72/93] build(deps): bump github.com/go-resty/resty/v2 from 2.4.0 to 2.7.0 (#1644) Bumps [github.com/go-resty/resty/v2](https://github.com/go-resty/resty) from 2.4.0 to 2.7.0. - [Release notes](https://github.com/go-resty/resty/releases) - [Commits](https://github.com/go-resty/resty/compare/v2.4.0...v2.7.0) --- updated-dependencies: - dependency-name: github.com/go-resty/resty/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7671b2da1fa..b2c36bffecc 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/aliyun/alibaba-cloud-sdk-go v1.62.156 github.com/davecgh/go-spew v1.1.1 github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0 - github.com/go-resty/resty/v2 v2.4.0 + github.com/go-resty/resty/v2 v2.7.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum index b46474bda68..91b67aef842 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= -github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k= -github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -857,6 +857,7 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= From 4f6bcdcee207e21aa115470ae6675dee83073938 Mon Sep 17 00:00:00 2001 From: rambohe Date: Tue, 15 Aug 2023 16:42:23 +0800 Subject: [PATCH 73/93] improve nodepool as following: (#1651) 1. add spec.HostNetwork field 2. remove spec.Selector field 3. improve node labels for nodepool(apps.openyurt.io/desired-nodepool label is removed) 4. support node conversion between v1alpha1 and v1beta1 version Signed-off-by: rambohe-ch --- Makefile | 20 +- .../crds/apps.openyurt.io_nodepools.yaml | 509 +++++------ .../yurt-manager-auto-generated.yaml | 26 +- cmd/yurt-manager/app/manager.go | 2 +- .../app/options/nodepoolcontroller.go | 6 +- go.mod | 2 +- hack/make-rules/kustomize_to_chart.sh | 8 + pkg/apis/apps/v1alpha1/nodepool_conversion.go | 49 +- pkg/apis/apps/v1alpha1/nodepool_types.go | 3 +- pkg/apis/apps/v1beta1/default.go | 44 - pkg/apis/apps/v1beta1/nodepool_conversion.go | 49 +- pkg/apis/apps/v1beta1/nodepool_types.go | 9 +- .../apps/v1beta1/zz_generated.deepcopy.go | 10 +- .../apps/well_known_labels_annotations.go | 13 +- pkg/controller/nodepool/config/types.go | 2 +- .../nodepool/nodepool_controller.go | 143 ++- .../nodepool/nodepool_controller_test.go | 297 ++++++ .../nodepool/nodepool_enqueue_handlers.go | 90 +- .../nodepool_enqueue_handlers_test.go | 319 +++++++ pkg/controller/nodepool/util.go | 206 +---- pkg/controller/nodepool/util_test.go | 859 ++++++++++++++++++ pkg/webhook/node/v1/node_default.go | 61 ++ pkg/webhook/node/v1/node_default_test.go | 125 +++ pkg/webhook/node/v1/node_handler.go | 9 +- pkg/webhook/node/v1/node_validation.go | 32 +- pkg/webhook/node/v1/node_validation_test.go | 156 ++++ .../nodepool/v1alpha1/nodepool_default.go | 50 - .../nodepool/v1alpha1/nodepool_handler.go | 51 -- .../nodepool/v1alpha1/nodepool_validation.go | 143 --- .../nodepool/v1beta1/nodepool_default.go | 17 +- .../nodepool/v1beta1/nodepool_default_test.go | 110 +++ .../nodepool/v1beta1/nodepool_handler.go | 4 +- .../nodepool/v1beta1/nodepool_validation.go | 23 +- .../v1beta1/nodepool_validation_test.go | 285 ++++++ pkg/webhook/server.go | 7 +- .../util/controller/webhook_controller.go | 95 +- pkg/yurthub/network/dummyif_test.go | 2 + 37 files changed, 2841 insertions(+), 995 deletions(-) delete mode 100644 pkg/apis/apps/v1beta1/default.go create mode 100644 pkg/controller/nodepool/nodepool_controller_test.go create mode 100644 pkg/controller/nodepool/nodepool_enqueue_handlers_test.go create mode 100644 pkg/controller/nodepool/util_test.go create mode 100644 pkg/webhook/node/v1/node_default.go create mode 100644 pkg/webhook/node/v1/node_default_test.go create mode 100644 pkg/webhook/node/v1/node_validation_test.go delete mode 100644 pkg/webhook/nodepool/v1alpha1/nodepool_default.go delete mode 100644 pkg/webhook/nodepool/v1alpha1/nodepool_handler.go delete mode 100644 pkg/webhook/nodepool/v1alpha1/nodepool_validation.go create mode 100644 pkg/webhook/nodepool/v1beta1/nodepool_default_test.go create mode 100644 pkg/webhook/nodepool/v1beta1/nodepool_validation_test.go diff --git a/Makefile b/Makefile index fefb7fe0ca0..bae9f1a4a1f 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,9 @@ KUSTOMIZE ?= $(LOCALBIN)/kustomize KUBECTL_VERSION ?= v1.22.3 KUBECTL ?= $(LOCALBIN)/kubectl +YQ_VERSION := 4.13.2 +YQ := $(shell command -v $(LOCALBIN)/yq 2> /dev/null) + .PHONY: clean all build test all: test build @@ -72,8 +75,8 @@ build: # Run test test: - go test -v -short ./pkg/... ./cmd/... -coverprofile cover.out - go test -v -coverpkg=./pkg/yurttunnel/... -coverprofile=yurttunnel-cover.out ./test/integration/yurttunnel_test.go + go test -v ./pkg/... ./cmd/... -coverprofile cover.out + go test -v -coverpkg=./pkg/yurttunnel/... -coverprofile=yurttunnel-cover.out ./test/integration/yurttunnel_test.go clean: -rm -Rf _output @@ -178,7 +181,7 @@ generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and # hack/make-rule/generate_openapi.sh // TODO by kadisi $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./pkg/apis/..." -manifests: kustomize kubectl generate ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. +manifests: kustomize kubectl yq generate ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. rm -rf $(BUILD_KUSTOMIZE) $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=role webhook paths="./pkg/..." output:crd:artifacts:config=$(BUILD_KUSTOMIZE)/auto_generate/crd output:rbac:artifacts:config=$(BUILD_KUSTOMIZE)/auto_generate/rbac output:webhook:artifacts:config=$(BUILD_KUSTOMIZE)/auto_generate/webhook hack/make-rules/kustomize_to_chart.sh --crd $(BUILD_KUSTOMIZE)/auto_generate/crd --webhook $(BUILD_KUSTOMIZE)/auto_generate/webhook --rbac $(BUILD_KUSTOMIZE)/auto_generate/rbac --output $(BUILD_KUSTOMIZE)/kustomize --chartDir charts/yurt-manager @@ -219,6 +222,16 @@ $(KUSTOMIZE): $(LOCALBIN) fi test -s $(LOCALBIN)/kustomize || { curl -Ss $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN); } +.PHONY: yq +yq: +ifndef YQ + @echo "Installing yq..." + test -s $(LOCALBIN)/yq || curl -k -L https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_$(shell go env GOOS)_$(shell go env GOARCH) -o $(LOCALBIN)/yq + chmod +x $(LOCALBIN)/yq +else + @echo "yq is already installed" +endif + # go-get-tool will 'go get' any package $2 and install it to $1. PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) define go-get-tool @@ -233,7 +246,6 @@ rm -rf $$TMP_DIR ;\ } endef - fmt: go fmt ./... find . -name '*.go' | grep -Ev 'vendor|thrift_gen' | xargs goimports -w diff --git a/charts/yurt-manager/crds/apps.openyurt.io_nodepools.yaml b/charts/yurt-manager/crds/apps.openyurt.io_nodepools.yaml index b7260163869..c9e0fc89c59 100644 --- a/charts/yurt-manager/crds/apps.openyurt.io_nodepools.yaml +++ b/charts/yurt-manager/crds/apps.openyurt.io_nodepools.yaml @@ -9,308 +9,241 @@ spec: group: apps.openyurt.io names: categories: - - all + - all kind: NodePool listKind: NodePoolList plural: nodepools shortNames: - - np + - np singular: nodepool scope: Cluster versions: - - additionalPrinterColumns: - - description: The type of nodepool - jsonPath: .spec.type - name: Type - type: string - - description: The number of ready nodes in the pool - jsonPath: .status.readyNodeNum - name: ReadyNodes - type: integer - - jsonPath: .status.unreadyNodeNum - name: NotReadyNodes - type: integer - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - deprecated: true - deprecationWarning: apps.openyurt.io/v1alpha1 NodePool is deprecated in v1.0.0+, - unavailable in v1.2.0+; use apps.openyurt.io/v1beta1 NodePool - name: v1alpha1 - schema: - openAPIV3Schema: - description: NodePool is the Schema for the nodepools API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: NodePoolSpec defines the desired state of NodePool - properties: - annotations: - additionalProperties: - type: string - description: 'If specified, the Annotations will be added to all nodes. - NOTE: existing labels with samy keys on the nodes will be overwritten.' - type: object - labels: - additionalProperties: - type: string - description: 'If specified, the Labels will be added to all nodes. - NOTE: existing labels with samy keys on the nodes will be overwritten.' - type: object - selector: - description: A label query over nodes to consider for adding to the - pool - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: A label selector requirement is a selector that - contains values, a key, and an operator that relates the key - and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: operator represents a key's relationship to - a set of values. Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of string values. If the - operator is In or NotIn, the values array must be non-empty. - If the operator is Exists or DoesNotExist, the values - array must be empty. This array is replaced during a strategic - merge patch. - items: + - additionalPrinterColumns: + - description: The type of nodepool + jsonPath: .spec.type + name: Type + type: string + - description: The number of ready nodes in the pool + jsonPath: .status.readyNodeNum + name: ReadyNodes + type: integer + - jsonPath: .status.unreadyNodeNum + name: NotReadyNodes + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: apps.openyurt.io/v1alpha1 NodePool is deprecated in v1.0.0+; use apps.openyurt.io/v1beta1 NodePool + name: v1alpha1 + schema: + openAPIV3Schema: + description: NodePool is the Schema for the nodepools API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: NodePoolSpec defines the desired state of NodePool + properties: + annotations: + additionalProperties: + type: string + description: 'If specified, the Annotations will be added to all nodes. NOTE: existing labels with samy keys on the nodes will be overwritten.' + type: object + labels: + additionalProperties: + type: string + description: 'If specified, the Labels will be added to all nodes. NOTE: existing labels with samy keys on the nodes will be overwritten.' + type: object + selector: + description: A label query over nodes to consider for adding to the pool + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. type: string - type: array - required: - - key - - operator + operator: + description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single - {key,value} in the matchLabels map is equivalent to an element - of matchExpressions, whose key field is "key", the operator - is "In", and the values array contains only "value". The requirements - are ANDed. - type: object - type: object - taints: - description: If specified, the Taints will be added to all nodes. - items: - description: The node this Taint is attached to has the "effect" - on any pod that does not tolerate the Taint. - properties: - effect: - description: Required. The effect of the taint on pods that - do not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule - and NoExecute. - type: string - key: - description: Required. The taint key to be applied to a node. - type: string - timeAdded: - description: TimeAdded represents the time at which the taint - was added. It is only written for NoExecute taints. - format: date-time - type: string - value: - description: The taint value corresponding to the taint key. - type: string - required: - - effect - - key type: object - type: array - type: - description: The type of the NodePool - type: string - type: object - status: - description: NodePoolStatus defines the observed state of NodePool - properties: - nodes: - description: The list of nodes' names in the pool - items: - type: string - type: array - readyNodeNum: - description: Total number of ready nodes in the pool. - format: int32 - type: integer - unreadyNodeNum: - description: Total number of unready nodes in the pool. - format: int32 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} - - additionalPrinterColumns: - - description: The type of nodepool - jsonPath: .spec.type - name: Type - type: string - - description: The number of ready nodes in the pool - jsonPath: .status.readyNodeNum - name: ReadyNodes - type: integer - - jsonPath: .status.unreadyNodeNum - name: NotReadyNodes - type: integer - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1beta1 - schema: - openAPIV3Schema: - description: NodePool is the Schema for the nodepools API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: NodePoolSpec defines the desired state of NodePool - properties: - annotations: - additionalProperties: - type: string - description: 'If specified, the Annotations will be added to all nodes. - NOTE: existing labels with samy keys on the nodes will be overwritten.' - type: object - labels: - additionalProperties: - type: string - description: 'If specified, the Labels will be added to all nodes. - NOTE: existing labels with samy keys on the nodes will be overwritten.' - type: object - selector: - description: A label query over nodes to consider for adding to the - pool - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. - The requirements are ANDed. - items: - description: A label selector requirement is a selector that - contains values, a key, and an operator that relates the key - and values. - properties: - key: - description: key is the label key that the selector applies - to. - type: string - operator: - description: operator represents a key's relationship to - a set of values. Valid operators are In, NotIn, Exists - and DoesNotExist. - type: string - values: - description: values is an array of string values. If the - operator is In or NotIn, the values array must be non-empty. - If the operator is Exists or DoesNotExist, the values - array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - required: + taints: + description: If specified, the Taints will be added to all nodes. + items: + description: The node this Taint is attached to has the "effect" on any pod that does not tolerate the Taint. + properties: + effect: + description: Required. The effect of the taint on pods that do not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Required. The taint key to be applied to a node. + type: string + timeAdded: + description: TimeAdded represents the time at which the taint was added. It is only written for NoExecute taints. + format: date-time + type: string + value: + description: The taint value corresponding to the taint key. + type: string + required: + - effect - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single - {key,value} in the matchLabels map is equivalent to an element - of matchExpressions, whose key field is "key", the operator - is "In", and the values array contains only "value". The requirements - are ANDed. type: object - type: object - taints: - description: If specified, the Taints will be added to all nodes. - items: - description: The node this Taint is attached to has the "effect" - on any pod that does not tolerate the Taint. - properties: - effect: - description: Required. The effect of the taint on pods that - do not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule - and NoExecute. - type: string - key: - description: Required. The taint key to be applied to a node. - type: string - timeAdded: - description: TimeAdded represents the time at which the taint - was added. It is only written for NoExecute taints. - format: date-time - type: string - value: - description: The taint value corresponding to the taint key. - type: string - required: - - effect - - key + type: array + type: + description: The type of the NodePool + type: string + type: object + status: + description: NodePoolStatus defines the observed state of NodePool + properties: + nodes: + description: The list of nodes' names in the pool + items: + type: string + type: array + readyNodeNum: + description: Total number of ready nodes in the pool. + format: int32 + type: integer + unreadyNodeNum: + description: Total number of unready nodes in the pool. + format: int32 + type: integer + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - description: The type of nodepool + jsonPath: .spec.type + name: Type + type: string + - description: The number of ready nodes in the pool + jsonPath: .status.readyNodeNum + name: ReadyNodes + type: integer + - jsonPath: .status.unreadyNodeNum + name: NotReadyNodes + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: NodePool is the Schema for the nodepools API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: NodePoolSpec defines the desired state of NodePool + properties: + annotations: + additionalProperties: + type: string + description: 'If specified, the Annotations will be added to all nodes. NOTE: existing labels with samy keys on the nodes will be overwritten.' + type: object + hostNetwork: + description: HostNetwork is used to specify that cni components(like flannel) will not be installed on the nodes of this NodePool. This means all pods on the nodes of this NodePool will use HostNetwork and share network namespace with host machine. + type: boolean + labels: + additionalProperties: + type: string + description: 'If specified, the Labels will be added to all nodes. NOTE: existing labels with samy keys on the nodes will be overwritten.' type: object - type: array - type: - description: The type of the NodePool - type: string - type: object - status: - description: NodePoolStatus defines the observed state of NodePool - properties: - nodes: - description: The list of nodes' names in the pool - items: + taints: + description: If specified, the Taints will be added to all nodes. + items: + description: The node this Taint is attached to has the "effect" on any pod that does not tolerate the Taint. + properties: + effect: + description: Required. The effect of the taint on pods that do not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Required. The taint key to be applied to a node. + type: string + timeAdded: + description: TimeAdded represents the time at which the taint was added. It is only written for NoExecute taints. + format: date-time + type: string + value: + description: The taint value corresponding to the taint key. + type: string + required: + - effect + - key + type: object + type: array + type: + description: The type of the NodePool type: string - type: array - readyNodeNum: - description: Total number of ready nodes in the pool. - format: int32 - type: integer - unreadyNodeNum: - description: Total number of unready nodes in the pool. - format: int32 - type: integer - type: object - type: object - served: true - storage: false - subresources: - status: {} + type: object + status: + description: NodePoolStatus defines the observed state of NodePool + properties: + nodes: + description: The list of nodes' names in the pool + items: + type: string + type: array + readyNodeNum: + description: Total number of ready nodes in the pool. + format: int32 + type: integer + unreadyNodeNum: + description: Total number of unready nodes in the pool. + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: + - v1beta1 + - v1alpha1 + clientConfig: + service: + namespace: kube-system + name: yurt-manager-webhook-service + path: /convert status: acceptedNames: kind: "" diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index 177e5500223..6faa1d2d0f1 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -126,8 +126,6 @@ rules: resources: - nodepools verbs: - - create - - delete - get - list - patch @@ -538,6 +536,27 @@ webhooks: sideEffects: None - admissionReviewVersions: - v1 + clientConfig: + service: + name: yurt-manager-webhook-service + namespace: {{ .Release.Namespace }} + path: /mutate-core-openyurt-io-v1-node + failurePolicy: Fail + name: mutate.core.v1.node.openyurt.io + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - nodes + sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 clientConfig: service: name: yurt-manager-webhook-service @@ -552,7 +571,6 @@ webhooks: - v1beta1 operations: - CREATE - - UPDATE resources: - nodepools sideEffects: None @@ -668,7 +686,6 @@ webhooks: sideEffects: None - admissionReviewVersions: - v1 - - v1beta1 clientConfig: service: name: yurt-manager-webhook-service @@ -688,6 +705,7 @@ webhooks: sideEffects: None - admissionReviewVersions: - v1 + - v1beta1 clientConfig: service: name: yurt-manager-webhook-service diff --git a/cmd/yurt-manager/app/manager.go b/cmd/yurt-manager/app/manager.go index 25ee89c57ab..fd328f28674 100644 --- a/cmd/yurt-manager/app/manager.go +++ b/cmd/yurt-manager/app/manager.go @@ -194,7 +194,7 @@ func Run(c *config.CompletedConfig, stopCh <-chan struct{}) error { // +kubebuilder:scaffold:builder setupLog.Info("initialize webhook") - if err := webhook.Initialize(ctx, c); err != nil { + if err := webhook.Initialize(ctx, c, mgr.GetConfig()); err != nil { setupLog.Error(err, "unable to initialize webhook") os.Exit(1) } diff --git a/cmd/yurt-manager/app/options/nodepoolcontroller.go b/cmd/yurt-manager/app/options/nodepoolcontroller.go index fa9c7238ee2..8760760ef17 100644 --- a/cmd/yurt-manager/app/options/nodepoolcontroller.go +++ b/cmd/yurt-manager/app/options/nodepoolcontroller.go @@ -29,7 +29,7 @@ type NodePoolControllerOptions struct { func NewNodePoolControllerOptions() *NodePoolControllerOptions { return &NodePoolControllerOptions{ &config.NodePoolControllerConfiguration{ - CreateDefaultPool: false, + EnableSyncNodePoolConfigurations: true, }, } } @@ -40,7 +40,7 @@ func (n *NodePoolControllerOptions) AddFlags(fs *pflag.FlagSet) { return } - fs.BoolVar(&n.CreateDefaultPool, "create-default-pool", n.CreateDefaultPool, "Create default cloud/edge pools if indicated.") + fs.BoolVar(&n.EnableSyncNodePoolConfigurations, "enable-sync-nodepool-configurations", n.EnableSyncNodePoolConfigurations, "enable to sync nodepool configurations(including labels, annotations, taints in spec) to nodes in the nodepool.") } // ApplyTo fills up nodepool config with options. @@ -48,7 +48,7 @@ func (o *NodePoolControllerOptions) ApplyTo(cfg *config.NodePoolControllerConfig if o == nil { return nil } - cfg.CreateDefaultPool = o.CreateDefaultPool + cfg.EnableSyncNodePoolConfigurations = o.EnableSyncNodePoolConfigurations return nil } diff --git a/go.mod b/go.mod index b2c36bffecc..4e81d05d27c 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.22.3 + k8s.io/apiextensions-apiserver v0.22.2 k8s.io/apimachinery v0.22.3 k8s.io/apiserver v0.22.3 k8s.io/cli-runtime v0.22.3 @@ -163,7 +164,6 @@ require ( gopkg.in/ini.v1 v1.66.2 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.22.2 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect diff --git a/hack/make-rules/kustomize_to_chart.sh b/hack/make-rules/kustomize_to_chart.sh index f8e0f98eff8..07545cec742 100755 --- a/hack/make-rules/kustomize_to_chart.sh +++ b/hack/make-rules/kustomize_to_chart.sh @@ -39,6 +39,8 @@ YURT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" SUFFIX="auto_generated" +Conversion_Files=("apps.openyurt.io_nodepools.yaml") + while [ $# -gt 0 ];do case $1 in --crd|-c) @@ -197,6 +199,12 @@ EOF mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_deviceservices.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_deviceservices.yaml mv ${crd_dir}/apiextensions.k8s.io_v1_customresourcedefinition_deviceprofiles.iot.openyurt.io.yaml ${crd_dir}/iot.openyurt.io_deviceprofiles.yaml + # add conversion for crds + for file in "${Conversion_Files[@]}" + do + ${YURT_ROOT}/bin/yq eval -i ".spec.conversion = {\"strategy\": \"Webhook\", \"webhook\": {\"conversionReviewVersions\": [\"v1beta1\", \"v1alpha1\"], \"clientConfig\": {\"service\": {\"namespace\": \"kube-system\", \"name\": \"yurt-manager-webhook-service\", \"path\": \"/convert\"}}}}" ${crd_dir}/$file + done + # rbac dir local rbac_kustomization_resources="" for file in ${YURT_ROOT}/${RBAC}/* diff --git a/pkg/apis/apps/v1alpha1/nodepool_conversion.go b/pkg/apis/apps/v1alpha1/nodepool_conversion.go index a34319a0332..6dca755fb0b 100644 --- a/pkg/apis/apps/v1alpha1/nodepool_conversion.go +++ b/pkg/apis/apps/v1alpha1/nodepool_conversion.go @@ -16,11 +16,46 @@ limitations under the License. package v1alpha1 -/* -Implementing the hub method is pretty easy -- we just have to add an empty -method called `Hub()` to serve as a -[marker](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub). -*/ +import ( + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/conversion" + + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" +) + +func (src *NodePool) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*v1beta1.NodePool) + + dst.ObjectMeta = src.ObjectMeta + + dst.Spec.Type = v1beta1.NodePoolType(src.Spec.Type) + dst.Spec.Labels = src.Spec.Labels + dst.Spec.Annotations = src.Spec.Annotations + dst.Spec.Taints = src.Spec.Taints + + dst.Status.ReadyNodeNum = src.Status.ReadyNodeNum + dst.Status.UnreadyNodeNum = src.Status.UnreadyNodeNum + dst.Status.Nodes = src.Status.Nodes + + klog.Infof("convert from v1alpha1 to v1beta1 for nodepool %s", dst.Name) + + return nil +} + +func (dst *NodePool) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*v1beta1.NodePool) + + dst.ObjectMeta = src.ObjectMeta + + dst.Spec.Type = NodePoolType(src.Spec.Type) + dst.Spec.Labels = src.Spec.Labels + dst.Spec.Annotations = src.Spec.Annotations + dst.Spec.Taints = src.Spec.Taints + + dst.Status.ReadyNodeNum = src.Status.ReadyNodeNum + dst.Status.UnreadyNodeNum = src.Status.UnreadyNodeNum + dst.Status.Nodes = src.Status.Nodes -// Hub marks this type as a conversion hub. -func (*NodePool) Hub() {} + klog.Infof("convert from v1beta1 to v1alpha1 for nodepool %s", dst.Name) + return nil +} diff --git a/pkg/apis/apps/v1alpha1/nodepool_types.go b/pkg/apis/apps/v1alpha1/nodepool_types.go index 548c5d90249..b19e0c8c984 100644 --- a/pkg/apis/apps/v1alpha1/nodepool_types.go +++ b/pkg/apis/apps/v1alpha1/nodepool_types.go @@ -73,12 +73,11 @@ type NodePoolStatus struct { // +kubebuilder:object:root=true // +kubebuilder:resource:scope=Cluster,path=nodepools,shortName=np,categories=all // +kubebuilder:subresource:status -// +kubebuilder:deprecatedversion:warning="apps.openyurt.io/v1alpha1 NodePool is deprecated in v1.0.0+, unavailable in v1.2.0+; use apps.openyurt.io/v1beta1 NodePool" +// +kubebuilder:deprecatedversion:warning="apps.openyurt.io/v1alpha1 NodePool is deprecated in v1.0.0+; use apps.openyurt.io/v1beta1 NodePool" // +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type",description="The type of nodepool" // +kubebuilder:printcolumn:name="ReadyNodes",type="integer",JSONPath=".status.readyNodeNum",description="The number of ready nodes in the pool" // +kubebuilder:printcolumn:name="NotReadyNodes",type="integer",JSONPath=".status.unreadyNodeNum" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" -// +kubebuilder:storageversion // NodePool is the Schema for the nodepools API type NodePool struct { diff --git a/pkg/apis/apps/v1beta1/default.go b/pkg/apis/apps/v1beta1/default.go deleted file mode 100644 index f52b64855c2..00000000000 --- a/pkg/apis/apps/v1beta1/default.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1beta1 - -import ( - "strings" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/openyurtio/openyurt/pkg/apis/apps" -) - -// SetDefaultsNodePool set default values for NodePool. -func SetDefaultsNodePool(obj *NodePool) { - // example for set default value for NodePool - if obj.Annotations == nil { - obj.Annotations = make(map[string]string) - } - - obj.Spec.Selector = &metav1.LabelSelector{ - MatchLabels: map[string]string{apps.LabelCurrentNodePool: obj.Name}, - } - - // add NodePool.Spec.Type to NodePool labels - if obj.Labels == nil { - obj.Labels = make(map[string]string) - } - obj.Labels[apps.NodePoolTypeLabelKey] = strings.ToLower(string(obj.Spec.Type)) - -} diff --git a/pkg/apis/apps/v1beta1/nodepool_conversion.go b/pkg/apis/apps/v1beta1/nodepool_conversion.go index fb6887bc42c..be5f0b3bf36 100644 --- a/pkg/apis/apps/v1beta1/nodepool_conversion.go +++ b/pkg/apis/apps/v1beta1/nodepool_conversion.go @@ -16,46 +16,11 @@ limitations under the License. package v1beta1 -import ( - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/conversion" - - "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" -) - -func (src *NodePool) ConvertTo(dstRaw conversion.Hub) error { - dst := dstRaw.(*v1alpha1.NodePool) - - dst.ObjectMeta = src.ObjectMeta - - dst.Spec.Type = v1alpha1.NodePoolType(src.Spec.Type) - dst.Spec.Selector = src.Spec.Selector - dst.Spec.Annotations = src.Spec.Annotations - dst.Spec.Taints = src.Spec.Taints - - dst.Status.ReadyNodeNum = src.Status.ReadyNodeNum - dst.Status.UnreadyNodeNum = src.Status.UnreadyNodeNum - dst.Status.Nodes = src.Status.Nodes - - klog.Infof("convert from v1beta1 to v1alpha1 for %s", dst.Name) - - return nil -} - -func (dst *NodePool) ConvertFrom(srcRaw conversion.Hub) error { - src := srcRaw.(*v1alpha1.NodePool) - - dst.ObjectMeta = src.ObjectMeta - - dst.Spec.Type = NodePoolType(src.Spec.Type) - dst.Spec.Selector = src.Spec.Selector - dst.Spec.Annotations = src.Spec.Annotations - dst.Spec.Taints = src.Spec.Taints - - dst.Status.ReadyNodeNum = src.Status.ReadyNodeNum - dst.Status.UnreadyNodeNum = src.Status.UnreadyNodeNum - dst.Status.Nodes = src.Status.Nodes +/* +Implementing the hub method is pretty easy -- we just have to add an empty +method called `Hub()` to serve as a +[marker](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub). +*/ - klog.Infof("convert from v1alpha1 to v1beta1 for %s", dst.Name) - return nil -} +// Hub marks this type as a conversion hub. +func (*NodePool) Hub() {} diff --git a/pkg/apis/apps/v1beta1/nodepool_types.go b/pkg/apis/apps/v1beta1/nodepool_types.go index bd970f4c164..60ee2a08b6c 100644 --- a/pkg/apis/apps/v1beta1/nodepool_types.go +++ b/pkg/apis/apps/v1beta1/nodepool_types.go @@ -36,9 +36,11 @@ type NodePoolSpec struct { // +optional Type NodePoolType `json:"type,omitempty"` - // A label query over nodes to consider for adding to the pool - // +optional - Selector *metav1.LabelSelector `json:"selector,omitempty"` + // HostNetwork is used to specify that cni components(like flannel) + // will not be installed on the nodes of this NodePool. + // This means all pods on the nodes of this NodePool will use + // HostNetwork and share network namespace with host machine. + HostNetwork bool `json:"hostNetwork,omitempty"` // If specified, the Labels will be added to all nodes. // NOTE: existing labels with samy keys on the nodes will be overwritten. @@ -80,6 +82,7 @@ type NodePoolStatus struct { // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:subresource:status // +genclient:nonNamespaced +// +kubebuilder:storageversion // NodePool is the Schema for the nodepools API type NodePool struct { diff --git a/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go b/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go index ebb32758258..aed2b7ae309 100644 --- a/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/apps/v1beta1/zz_generated.deepcopy.go @@ -22,8 +22,7 @@ limitations under the License. package v1beta1 import ( - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -89,11 +88,6 @@ func (in *NodePoolList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodePoolSpec) DeepCopyInto(out *NodePoolSpec) { *out = *in - if in.Selector != nil { - in, out := &in.Selector, &out.Selector - *out = new(v1.LabelSelector) - (*in).DeepCopyInto(*out) - } if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) @@ -110,7 +104,7 @@ func (in *NodePoolSpec) DeepCopyInto(out *NodePoolSpec) { } if in.Taints != nil { in, out := &in.Taints, &out.Taints - *out = make([]corev1.Taint, len(*in)) + *out = make([]v1.Taint, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/pkg/apis/apps/well_known_labels_annotations.go b/pkg/apis/apps/well_known_labels_annotations.go index 7118efd71d4..56f7abb77e4 100644 --- a/pkg/apis/apps/well_known_labels_annotations.go +++ b/pkg/apis/apps/well_known_labels_annotations.go @@ -40,17 +40,12 @@ const ( // LabelCurrentNodePool indicates which nodepool the node is currently // belonging to LabelCurrentNodePool = "apps.openyurt.io/nodepool" - NodePoolLabel = "apps.openyurt.io/nodepool" AnnotationPrevAttrs = "nodepool.openyurt.io/previous-attributes" - // DefaultCloudNodePoolName defines the name of the default cloud nodepool - DefaultCloudNodePoolName = "default-nodepool" + NodePoolLabel = "apps.openyurt.io/nodepool" + NodePoolTypeLabel = "nodepool.openyurt.io/type" + NodePoolHostNetworkLabel = "nodepool.openyurt.io/hostnetwork" - // DefaultEdgeNodePoolName defines the name of the default edge nodepool - DefaultEdgeNodePoolName = "default-edge-nodepool" - - // ServiceTopologyKey is the toplogy key that will be attached to node, - // the value will be the name of the nodepool - ServiceTopologyKey = "topology.kubernetes.io/zone" + NodePoolChangedEvent = "NodePoolChanged" ) diff --git a/pkg/controller/nodepool/config/types.go b/pkg/controller/nodepool/config/types.go index baedb88cf67..4ce71aa87c9 100644 --- a/pkg/controller/nodepool/config/types.go +++ b/pkg/controller/nodepool/config/types.go @@ -18,5 +18,5 @@ package config // NodePoolControllerConfiguration contains elements describing NodePoolController. type NodePoolControllerConfiguration struct { - CreateDefaultPool bool + EnableSyncNodePoolConfigurations bool } diff --git a/pkg/controller/nodepool/nodepool_controller.go b/pkg/controller/nodepool/nodepool_controller.go index ca0965105f6..2ca6d37ea35 100644 --- a/pkg/controller/nodepool/nodepool_controller.go +++ b/pkg/controller/nodepool/nodepool_controller.go @@ -18,11 +18,10 @@ package nodepool import ( "context" - "errors" "fmt" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" @@ -36,14 +35,12 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" - nodepoolconfig "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" - utilclient "github.com/openyurtio/openyurt/pkg/util/client" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" + poolconfig "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" ) var ( concurrentReconciles = 3 - controllerKind = appsv1beta1.SchemeGroupVersion.WithKind("NodePool") + controllerResource = appsv1beta1.SchemeGroupVersion.WithResource("nodepools") ) const ( @@ -58,9 +55,19 @@ func Format(format string, args ...interface{}) string { // ReconcileNodePool reconciles a NodePool object type ReconcileNodePool struct { client.Client - scheme *runtime.Scheme - recorder record.EventRecorder - Configration nodepoolconfig.NodePoolControllerConfiguration + mapper meta.RESTMapper + recorder record.EventRecorder + cfg poolconfig.NodePoolControllerConfiguration +} + +func (r *ReconcileNodePool) InjectClient(c client.Client) error { + r.Client = c + return nil +} + +func (r *ReconcileNodePool) InjectMapper(mapper meta.RESTMapper) error { + r.mapper = mapper + return nil } var _ reconcile.Reconciler = &ReconcileNodePool{} @@ -68,71 +75,53 @@ var _ reconcile.Reconciler = &ReconcileNodePool{} // Add creates a new NodePool Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(c *config.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - klog.Errorf(Format("DiscoverGVK error")) - return nil + klog.Infof("nodepool-controller add controller %s", controllerResource.String()) + r := &ReconcileNodePool{ + cfg: c.ComponentConfig.NodePoolController, + recorder: mgr.GetEventRecorderFor(ControllerName), } - klog.Infof("nodepool-controller add controller %s", controllerKind.String()) - return add(mgr, newReconciler(c, mgr)) -} - -type NodePoolRelatedAttributes struct { - Labels map[string]string `json:"labels,omitempty"` - Annotations map[string]string `json:"annotations,omitempty"` - Taints []corev1.Taint `json:"taints,omitempty"` -} - -// newReconciler returns a new reconcile.Reconciler -func newReconciler(c *config.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { - return &ReconcileNodePool{ - Client: utilclient.NewClientFromManager(mgr, ControllerName), - scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(ControllerName), - Configration: c.ComponentConfig.NodePoolController, - } -} - -// add adds a new Controller to mgr with r as the reconcile.Reconciler -func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(ControllerName, mgr, controller.Options{ + ctrl, err := controller.New(ControllerName, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, }) if err != nil { return err } + if _, err := r.mapper.KindFor(controllerResource); err != nil { + klog.Infof("resource %s doesn't exist", controllerResource.String()) + return err + } + // Watch for changes to NodePool - err = c.Watch(&source.Kind{Type: &appsv1beta1.NodePool{}}, &handler.EnqueueRequestForObject{}) + err = ctrl.Watch(&source.Kind{Type: &appsv1beta1.NodePool{}}, &handler.EnqueueRequestForObject{}) if err != nil { return err } // Watch for changes to Node - err = c.Watch(&source.Kind{ - Type: &corev1.Node{}}, - &EnqueueNodePoolForNode{}) + err = ctrl.Watch(&source.Kind{Type: &corev1.Node{}}, &EnqueueNodePoolForNode{ + EnableSyncNodePoolConfigurations: r.cfg.EnableSyncNodePoolConfigurations, + Recorder: r.recorder, + }) if err != nil { return err } - npr, ok := r.(*ReconcileNodePool) - if !ok { - return errors.New(Format("fail to assert interface to NodePoolReconciler")) - } - - if npr.Configration.CreateDefaultPool { - // register a node controller with the underlying informer of the manager - go createDefaultNodePool(mgr.GetClient()) - } return nil + +} + +type NodePoolRelatedAttributes struct { + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + Taints []corev1.Taint `json:"taints,omitempty"` } -// +kubebuilder:rbac:groups=apps.openyurt.io,resources=nodepools,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps.openyurt.io,resources=nodepools,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=apps.openyurt.io,resources=nodepools/status,verbs=get;update;patch // +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch;update;patch -// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete // Reconcile reads that state of the cluster for a NodePool object and makes changes based on the state read // and what is in the NodePool.Spec @@ -141,51 +130,30 @@ func (r *ReconcileNodePool) Reconcile(ctx context.Context, req reconcile.Request // Note !!!!!!!!!! // We strongly recommend use Format() to encapsulation because Format() can print logs by module // @kadisi - klog.Infof(Format("Reconcile NodePool %s/%s", req.Namespace, req.Name)) + klog.Infof(Format("Reconcile NodePool %s", req.Name)) var nodePool appsv1beta1.NodePool // try to reconcile the NodePool object if err := r.Get(ctx, req.NamespacedName, &nodePool); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - klog.Infof(Format("NodePool %++v", nodePool)) - - var desiredNodeList corev1.NodeList - if err := r.List(ctx, &desiredNodeList, client.MatchingLabels(map[string]string{ - apps.LabelDesiredNodePool: nodePool.GetName(), - })); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } + klog.V(5).Infof("NodePool %s: %#+v", nodePool.Name, nodePool) var currentNodeList corev1.NodeList if err := r.List(ctx, ¤tNodeList, client.MatchingLabels(map[string]string{ - apps.LabelCurrentNodePool: nodePool.GetName(), + apps.NodePoolLabel: nodePool.GetName(), })); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - // 1. handle the event of removing node out of the pool - // nodes in currentNodeList but not in the desiredNodeList, will be - // removed from the pool - removedNodes := getRemovedNodes(¤tNodeList, &desiredNodeList) - for _, rNode := range removedNodes { - if err := removePoolRelatedAttrs(&rNode); err != nil { - return ctrl.Result{}, err - } - if err := r.Update(ctx, &rNode); err != nil { - return ctrl.Result{}, err - } - } - - // 2. handle the event of adding node to the pool and the event of - // updating node pool attributes var ( readyNode int32 notReadyNode int32 nodes []string ) - for _, node := range desiredNodeList.Items { + // sync nodepool configurations to nodes + for _, node := range currentNodeList.Items { // prepare nodepool status nodes = append(nodes, node.GetName()) if isNodeReady(node) { @@ -194,23 +162,28 @@ func (r *ReconcileNodePool) Reconcile(ctx context.Context, req reconcile.Request notReadyNode += 1 } - // update node status according to nodepool - updated, err := concilateNode(&node, nodePool) - if err != nil { - return ctrl.Result{}, err - } - if updated { - if err := r.Update(ctx, &node); err != nil { - klog.Errorf(Format("Update Node %s error %v", node.Name, err)) + // sync nodepool configurations into node + if r.cfg.EnableSyncNodePoolConfigurations { + updated, err := conciliateNode(&node, &nodePool) + if err != nil { return ctrl.Result{}, err } + if updated { + if err := r.Update(ctx, &node); err != nil { + klog.Errorf(Format("Update Node %s error %v", node.Name, err)) + return ctrl.Result{}, err + } + } } } - // 3. always update the node pool status if necessary + // always update the node pool status if necessary needUpdate := conciliateNodePoolStatus(readyNode, notReadyNode, nodes, &nodePool) if needUpdate { + klog.V(5).Infof("nodepool(%s): (%#+v) will be updated", nodePool.Name, nodePool) return ctrl.Result{}, r.Status().Update(ctx, &nodePool) + } else { + klog.V(5).Infof("nodepool(%#+v) don't need to be updated, ready=%d, notReady=%d, nodes=%v", nodePool, readyNode, notReadyNode, nodes) } return ctrl.Result{}, nil } diff --git a/pkg/controller/nodepool/nodepool_controller_test.go b/pkg/controller/nodepool/nodepool_controller_test.go new file mode 100644 index 00000000000..aa0d0c687fb --- /dev/null +++ b/pkg/controller/nodepool/nodepool_controller_test.go @@ -0,0 +1,297 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nodepool + +import ( + "context" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/apis/apps" + appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" + poolconfig "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" +) + +func prepareNodes() []client.Object { + nodes := []client.Object{ + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeNetworkUnavailable, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node3", + Labels: map[string]string{ + apps.NodePoolLabel: "beijing", + }, + }, + }, + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node4", + Labels: map[string]string{ + apps.NodePoolLabel: "beijing", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + return nodes +} + +func prepareNodePools() []client.Object { + pools := []client.Object{ + &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hangzhou", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "region": "hangzhou", + }, + }, + }, + &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "beijing", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "region": "beijing", + }, + }, + }, + &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "shanghai", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "region": "shanghai", + }, + }, + }, + } + return pools +} + +func TestReconcile(t *testing.T) { + nodes := prepareNodes() + pools := prepareNodePools() + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatal("Fail to add kubernetes clint-go custom resource") + } + apis.AddToScheme(scheme) + + c := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(pools...).WithObjects(nodes...).Build() + testcases := map[string]struct { + EnableSyncNodePoolConfigurations bool + pool string + wantedPool *appsv1beta1.NodePool + wantedNodes []corev1.Node + err error + }{ + "reconcile hangzhou pool": { + pool: "hangzhou", + wantedPool: &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hangzhou", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "region": "hangzhou", + }, + }, + Status: appsv1beta1.NodePoolStatus{ + ReadyNodeNum: 1, + UnreadyNodeNum: 1, + Nodes: []string{"node1", "node2"}, + }, + }, + }, + "reconcile beijing pool": { + EnableSyncNodePoolConfigurations: true, + pool: "beijing", + wantedPool: &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "beijing", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "region": "beijing", + }, + }, + Status: appsv1beta1.NodePoolStatus{ + ReadyNodeNum: 1, + UnreadyNodeNum: 1, + Nodes: []string{"node3", "node4"}, + }, + }, + wantedNodes: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node3", + Labels: map[string]string{ + apps.NodePoolLabel: "beijing", + "region": "beijing", + }, + Annotations: map[string]string{ + "nodepool.openyurt.io/previous-attributes": "{\"labels\":{\"region\":\"beijing\"}}", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node4", + Labels: map[string]string{ + apps.NodePoolLabel: "beijing", + "region": "beijing", + }, + Annotations: map[string]string{ + "nodepool.openyurt.io/previous-attributes": "{\"labels\":{\"region\":\"beijing\"}}", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + }, + }, + "reconcile shanghai pool without nodes": { + pool: "shanghai", + wantedPool: &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "shanghai", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "region": "shanghai", + }, + }, + Status: appsv1beta1.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + }, + }, + }, + } + + ctx := context.TODO() + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + r := &ReconcileNodePool{ + Client: c, + cfg: poolconfig.NodePoolControllerConfiguration{ + EnableSyncNodePoolConfigurations: tc.EnableSyncNodePoolConfigurations, + }, + } + req := reconcile.Request{NamespacedName: types.NamespacedName{Name: tc.pool}} + _, err := r.Reconcile(ctx, req) + if err != nil { + t.Errorf("Reconcile() error = %v", err) + return + } + + var wantedPool appsv1beta1.NodePool + if err := r.Get(ctx, req.NamespacedName, &wantedPool); err != nil { + t.Errorf("Reconcile() error = %v", err) + return + } + if !reflect.DeepEqual(wantedPool.Status, tc.wantedPool.Status) { + t.Errorf("expected %#+v, got %#+v", tc.wantedPool.Status, wantedPool.Status) + return + } + + if len(tc.wantedNodes) != 0 { + var currentNodeList corev1.NodeList + if err := r.List(ctx, ¤tNodeList, client.MatchingLabels(map[string]string{ + apps.NodePoolLabel: tc.pool, + })); err != nil { + t.Errorf("Reconcile() error = %v", err) + return + } + gotNodes := make([]corev1.Node, 0) + for i := range currentNodeList.Items { + node := currentNodeList.Items[i] + node.ObjectMeta.ResourceVersion = "" + gotNodes = append(gotNodes, node) + } + + if !reflect.DeepEqual(tc.wantedNodes, gotNodes) { + t.Errorf("expected %#+v, \ngot %#+v", tc.wantedNodes, gotNodes) + } + } + }) + } +} diff --git a/pkg/controller/nodepool/nodepool_enqueue_handlers.go b/pkg/controller/nodepool/nodepool_enqueue_handlers.go index b19151043f4..6fe91665d6b 100644 --- a/pkg/controller/nodepool/nodepool_enqueue_handlers.go +++ b/pkg/controller/nodepool/nodepool_enqueue_handlers.go @@ -17,17 +17,24 @@ limitations under the License. package nodepool import ( + "fmt" "reflect" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/openyurtio/openyurt/pkg/apis/apps" ) -type EnqueueNodePoolForNode struct{} +type EnqueueNodePoolForNode struct { + EnableSyncNodePoolConfigurations bool + Recorder record.EventRecorder +} // Create implements EventHandler func (e *EnqueueNodePoolForNode) Create(evt event.CreateEvent, @@ -39,7 +46,7 @@ func (e *EnqueueNodePoolForNode) Create(evt event.CreateEvent, } klog.V(5).Infof(Format("will enqueue nodepool as node(%s) has been created", node.GetName())) - if np, exist := node.Labels[apps.LabelDesiredNodePool]; exist { + if np := node.Labels[apps.NodePoolLabel]; len(np) != 0 { addNodePoolToWorkQueue(np, q) return } @@ -61,59 +68,44 @@ func (e *EnqueueNodePoolForNode) Update(evt event.UpdateEvent, evt.ObjectOld.GetName())) return } - klog.V(5).Infof(Format("Will enqueue nodepool as node(%s) has been updated", - newNode.GetName())) - newNp := newNode.Labels[apps.LabelDesiredNodePool] - oldNp := oldNode.Labels[apps.LabelCurrentNodePool] + newNp := newNode.Labels[apps.NodePoolLabel] + oldNp := oldNode.Labels[apps.NodePoolLabel] + + // check the NodePoolLabel of node if len(oldNp) == 0 && len(newNp) == 0 { - klog.V(4).Infof(Format("node(%s) does not belong to any nodepool", newNode.GetName())) return - } - - if newNp != oldNp { - if newNp == "" { - // remove node from old pool - klog.V(5).Infof(Format("Will enqueue old pool(%s) for node(%s)", - oldNp, newNode.GetName())) - addNodePoolToWorkQueue(oldNp, q) - return - } - - if oldNp == "" { - // add node to the new Pool - klog.V(5).Infof(Format("Will enqueue new pool(%s) for node(%s)", - newNp, newNode.GetName())) - addNodePoolToWorkQueue(newNp, q) - return - } - klog.V(5).Infof(Format("Will enqueue both new pool(%s) and"+ - " old pool(%s) for node(%s)", - newNp, oldNp, newNode.GetName())) - addNodePoolToWorkQueue(oldNp, q) + } else if len(oldNp) == 0 { + // add node to the new Pool + klog.V(4).Infof(Format("node(%s) is added into pool(%s)", newNode.Name, newNp)) addNodePoolToWorkQueue(newNp, q) return + } else if oldNp != newNp { + klog.Warningf("It is not allowed to change the NodePoolLabel of node, but pool of node(%s) is changed from %s to %s", newNode.Name, oldNp, newNp) + // emit a warning event + e.Recorder.Event(newNode.DeepCopy(), corev1.EventTypeWarning, apps.NodePoolChangedEvent, + fmt.Sprintf("It is not allowed to change the NodePoolLabel of node, but nodepool of node(%s) is changed from %s to %s", newNode.Name, oldNp, newNp)) + return } + // check node ready status if isNodeReady(*newNode) != isNodeReady(*oldNode) { - // if the newNode and oldNode status are different - klog.V(5).Infof(Format("Node phase has been changed,"+ + klog.V(4).Infof(Format("Node ready status has been changed,"+ " will enqueue pool(%s) for node(%s)", newNp, newNode.GetName())) addNodePoolToWorkQueue(newNp, q) return } - if !reflect.DeepEqual(newNode.Labels, oldNode.Labels) || - !reflect.DeepEqual(newNode.Annotations, oldNode.Annotations) || - !reflect.DeepEqual(newNode.Spec.Taints, oldNode.Spec.Taints) { - // if node's labels, annotations or taints are updated - // TODO only consider the pool realted attributes - klog.V(5).Infof(Format("Nodepool related attributes has been changed,"+ - " will enqueue pool(%s) for node(%s)", - newNp, newNode.GetName())) - addNodePoolToWorkQueue(newNp, q) + // check node's labels, annotations or taints are updated or not + if e.EnableSyncNodePoolConfigurations { + if !reflect.DeepEqual(newNode.Labels, oldNode.Labels) || + !reflect.DeepEqual(newNode.Annotations, oldNode.Annotations) || + !reflect.DeepEqual(newNode.Spec.Taints, oldNode.Spec.Taints) { + // TODO only consider the pool related attributes + klog.V(5).Infof(Format("NodePool related attributes has been changed,will enqueue pool(%s) for node(%s)", newNp, newNode.Name)) + addNodePoolToWorkQueue(newNp, q) + } } - } // Delete implements EventHandler @@ -125,13 +117,13 @@ func (e *EnqueueNodePoolForNode) Delete(evt event.DeleteEvent, return } - np := node.Labels[apps.LabelCurrentNodePool] - if np == "" { - klog.V(5).Infof(Format("Node(%s) doesn't belong to any pool", node.GetName())) + np := node.Labels[apps.NodePoolLabel] + if len(np) == 0 { + klog.V(4).Infof(Format("A orphan node(%s) is removed", node.Name)) return } // enqueue the nodepool that the node belongs to - klog.V(5).Infof(Format("Will enqueue pool(%s) as node(%s) has been deleted", + klog.V(4).Infof(Format("Will enqueue pool(%s) as node(%s) has been deleted", np, node.GetName())) addNodePoolToWorkQueue(np, q) } @@ -140,3 +132,11 @@ func (e *EnqueueNodePoolForNode) Delete(evt event.DeleteEvent, func (e *EnqueueNodePoolForNode) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) { } + +// addNodePoolToWorkQueue adds the nodepool the reconciler's workqueue +func addNodePoolToWorkQueue(npName string, + q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{Name: npName}, + }) +} diff --git a/pkg/controller/nodepool/nodepool_enqueue_handlers_test.go b/pkg/controller/nodepool/nodepool_enqueue_handlers_test.go new file mode 100644 index 00000000000..67ed2a6a706 --- /dev/null +++ b/pkg/controller/nodepool/nodepool_enqueue_handlers_test.go @@ -0,0 +1,319 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nodepool + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + + "github.com/openyurtio/openyurt/pkg/apis/apps" +) + +func TestCreate(t *testing.T) { + testcases := map[string]struct { + event event.CreateEvent + wantedNum int + }{ + "add a pod": { + event: event.CreateEvent{ + Object: &corev1.Pod{}, + }, + wantedNum: 0, + }, + "node doesn't belong to a pool": { + event: event.CreateEvent{ + Object: &corev1.Node{}, + }, + wantedNum: 0, + }, + "node belongs to a pool": { + event: event.CreateEvent{ + Object: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + }, + }, + wantedNum: 1, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + handler := &EnqueueNodePoolForNode{} + q := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + handler.Create(tc.event, q) + + if q.Len() != tc.wantedNum { + t.Errorf("Expected %d, got %d", tc.wantedNum, q.Len()) + } + }) + } +} + +func TestUpdate(t *testing.T) { + testcases := map[string]struct { + event event.UpdateEvent + wantedNum int + }{ + "invalid old object": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Pod{}, + ObjectNew: &corev1.Node{}, + }, + wantedNum: 0, + }, + "invalid new object": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{}, + ObjectNew: &corev1.Pod{}, + }, + wantedNum: 0, + }, + "update orphan node": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{}, + ObjectNew: &corev1.Node{}, + }, + wantedNum: 0, + }, + "add a node into pool": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{}, + ObjectNew: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + }, + }, + wantedNum: 1, + }, + "pool of node is changed": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + }, + ObjectNew: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "bar", + }, + }, + }, + }, + wantedNum: 0, + }, + "pool of node is not changed": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + }, + ObjectNew: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + }, + }, + wantedNum: 0, + }, + "node ready status is changed": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionFalse, + }, + }, + }, + }, + ObjectNew: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + }, + wantedNum: 1, + }, + "node ready status is not changed": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + ObjectNew: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + }, + wantedNum: 0, + }, + "node labels is changed": { + event: event.UpdateEvent{ + ObjectOld: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + "label1": "value1", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + ObjectNew: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + "label2": "value2", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + }, + wantedNum: 1, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + handler := &EnqueueNodePoolForNode{ + EnableSyncNodePoolConfigurations: true, + Recorder: record.NewFakeRecorder(100), + } + q := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + handler.Update(tc.event, q) + + if q.Len() != tc.wantedNum { + t.Errorf("Expected %d, got %d", tc.wantedNum, q.Len()) + } + }) + } +} + +func TestDelete(t *testing.T) { + testcases := map[string]struct { + event event.DeleteEvent + wantedNum int + }{ + "delete a pod": { + event: event.DeleteEvent{ + Object: &corev1.Pod{}, + }, + wantedNum: 0, + }, + "delete a orphan node": { + event: event.DeleteEvent{ + Object: &corev1.Node{}, + }, + wantedNum: 0, + }, + "delete a pool node": { + event: event.DeleteEvent{ + Object: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "foo", + }, + }, + }, + }, + wantedNum: 1, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + handler := &EnqueueNodePoolForNode{} + q := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + handler.Delete(tc.event, q) + + if q.Len() != tc.wantedNum { + t.Errorf("Expected %d, got %d", tc.wantedNum, q.Len()) + } + }) + } +} diff --git a/pkg/controller/nodepool/util.go b/pkg/controller/nodepool/util.go index fc8b69ad333..dd816cde1d8 100644 --- a/pkg/controller/nodepool/util.go +++ b/pkg/controller/nodepool/util.go @@ -17,174 +17,44 @@ limitations under the License. package nodepool import ( - "context" "encoding/json" "reflect" "sort" - "time" corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/workqueue" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" ) -var timeSleep = time.Sleep - -// createNodePool creates an nodepool, it will retry 5 times if it fails -func createNodePool(c client.Client, name string, - poolType appsv1beta1.NodePoolType) bool { - for i := 0; i < 5; i++ { - np := appsv1beta1.NodePool{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: appsv1beta1.NodePoolSpec{ - Type: poolType, - }, - } - err := c.Create(context.TODO(), &np) - if err == nil { - klog.V(4).Infof("the default nodepool(%s) is created", name) - return true - } - if apierrors.IsAlreadyExists(err) { - klog.V(4).Infof("the default nodepool(%s) already exist", name) - return false - } - klog.Errorf("fail to create the node pool(%s): %s", name, err) - timeSleep(2 * time.Second) - } - klog.V(4).Info("fail to create the default nodepool after trying for 5 times") - return false -} - -// createDefaultNodePool creates the default NodePool if not exist -func createDefaultNodePool(client client.Client) { - createNodePool(client, - apps.DefaultEdgeNodePoolName, appsv1beta1.Edge) - createNodePool(client, - apps.DefaultCloudNodePoolName, appsv1beta1.Cloud) -} - // conciliatePoolRelatedAttrs will update the node's attributes that related to // the nodepool -func concilateNode(node *corev1.Node, nodePool appsv1beta1.NodePool) (attrUpdated bool, err error) { +func conciliateNode(node *corev1.Node, nodePool *appsv1beta1.NodePool) (bool, error) { // update node attr - npra := NodePoolRelatedAttributes{ + newNpra := &NodePoolRelatedAttributes{ Labels: nodePool.Spec.Labels, Annotations: nodePool.Spec.Annotations, Taints: nodePool.Spec.Taints, } - if preAttrs, exist := node.Annotations[apps.AnnotationPrevAttrs]; !exist { - node.Labels = mergeMap(node.Labels, npra.Labels) - node.Annotations = mergeMap(node.Annotations, npra.Annotations) - for _, npt := range npra.Taints { - for i, nt := range node.Spec.Taints { - if npt.Effect == nt.Effect && npt.Key == nt.Key { - node.Spec.Taints = append(node.Spec.Taints[:i], node.Spec.Taints[i+1:]...) - break - } - } - node.Spec.Taints = append(node.Spec.Taints, npt) - } - if err := cachePrevPoolAttrs(node, npra); err != nil { - return attrUpdated, err - } - attrUpdated = true - } else { - var preNpra NodePoolRelatedAttributes - if err := json.Unmarshal([]byte(preAttrs), &preNpra); err != nil { - return attrUpdated, err - } - if !reflect.DeepEqual(preNpra, npra) { - // pool related attributes will be updated - conciliateLabels(node, preNpra.Labels, npra.Labels) - conciliateAnnotations(node, preNpra.Annotations, npra.Annotations) - conciliateTaints(node, preNpra.Taints, npra.Taints) - if err := cachePrevPoolAttrs(node, npra); err != nil { - return attrUpdated, err - } - attrUpdated = true - } - } - - // update ownerLabel - if node.Labels[apps.LabelCurrentNodePool] != nodePool.GetName() { - if len(node.Labels) == 0 { - node.Labels = make(map[string]string) - } - node.Labels[apps.LabelCurrentNodePool] = nodePool.GetName() - attrUpdated = true - } - return attrUpdated, nil -} - -// getRemovedNodes calculates removed nodes from current nodes and desired nodes -func getRemovedNodes(currentNodeList *corev1.NodeList, desiredNodeList *corev1.NodeList) []corev1.Node { - var removedNodes []corev1.Node - for _, mNode := range currentNodeList.Items { - var found bool - for _, dNode := range desiredNodeList.Items { - if mNode.GetName() == dNode.GetName() { - found = true - break - } - } - if !found { - removedNodes = append(removedNodes, mNode) - } - } - return removedNodes -} - -// removePoolRelatedAttrs removes attributes(label/annotation/taint) that -// relate to nodepool -func removePoolRelatedAttrs(node *corev1.Node) error { - var npra NodePoolRelatedAttributes - - if _, exist := node.Annotations[apps.AnnotationPrevAttrs]; !exist { - return nil - } - - if err := json.Unmarshal( - []byte(node.Annotations[apps.AnnotationPrevAttrs]), - &npra); err != nil { - return err - } - - for lk, lv := range npra.Labels { - if node.Labels[lk] == lv { - delete(node.Labels, lk) - } - } - - for ak, av := range npra.Annotations { - if node.Annotations[ak] == av { - delete(node.Annotations, ak) - } + oldNpra, err := decodePoolAttrs(node) + if err != nil { + return false, err } - for _, t := range npra.Taints { - if i, exist := containTaint(t, node.Spec.Taints); exist { - node.Spec.Taints = append( - node.Spec.Taints[:i], - node.Spec.Taints[i+1:]...) + if !areNodePoolRelatedAttributesEqual(oldNpra, newNpra) { + //klog.Infof("oldNpra: %#+v, \n newNpra: %#+v", oldNpra, newNpra) + conciliateLabels(node, oldNpra.Labels, newNpra.Labels) + conciliateAnnotations(node, oldNpra.Annotations, newNpra.Annotations) + conciliateTaints(node, oldNpra.Taints, newNpra.Taints) + if err := encodePoolAttrs(node, newNpra); err != nil { + return false, err } + return true, nil } - delete(node.Annotations, apps.AnnotationPrevAttrs) - delete(node.Labels, apps.LabelCurrentNodePool) - return nil + return false, nil } // conciliateLabels will update the node's label that related to the nodepool @@ -219,17 +89,18 @@ func conciliateAnnotations(node *corev1.Node, oldAnnos, newAnnos map[string]stri // conciliateLabels will update the node's taint that related to the nodepool func conciliateTaints(node *corev1.Node, oldTaints, newTaints []corev1.Taint) { - // 1. remove taints from the node if they have been removed from the - // node pool + // 1. remove old taints from the node for _, oldTaint := range oldTaints { if _, exist := containTaint(oldTaint, node.Spec.Taints); exist { node.Spec.Taints = removeTaint(oldTaint, node.Spec.Taints) } } - // 2. update the node taints based on the latest node pool taints + // 2. add new node taints based on the latest node pool taints for _, nt := range newTaints { - node.Spec.Taints = append(node.Spec.Taints, nt) + if _, exist := containTaint(nt, node.Spec.Taints); !exist { + node.Spec.Taints = append(node.Spec.Taints, nt) + } } } @@ -253,7 +124,7 @@ func conciliateNodePoolStatus( // update the node list on demand sort.Strings(nodes) sort.Strings(nodePool.Status.Nodes) - if !reflect.DeepEqual(nodes, nodePool.Status.Nodes) { + if !(len(nodes) == 0 && len(nodePool.Status.Nodes) == 0 || reflect.DeepEqual(nodes, nodePool.Status.Nodes)) { nodePool.Status.Nodes = nodes needUpdate = true } @@ -301,10 +172,10 @@ func removeTaint(taint corev1.Taint, taints []corev1.Taint) []corev1.Taint { return taints } -// cachePrevPoolAttrs caches the nodepool-related attributes to the +// encodePoolAttrs caches the nodepool-related attributes to the // node's annotation -func cachePrevPoolAttrs(node *corev1.Node, - npra NodePoolRelatedAttributes) error { +func encodePoolAttrs(node *corev1.Node, + npra *NodePoolRelatedAttributes) error { npraJson, err := json.Marshal(npra) if err != nil { return err @@ -316,10 +187,29 @@ func cachePrevPoolAttrs(node *corev1.Node, return nil } -// addNodePoolToWorkQueue adds the nodepool the reconciler's workqueue -func addNodePoolToWorkQueue(npName string, - q workqueue.RateLimitingInterface) { - q.Add(reconcile.Request{ - NamespacedName: types.NamespacedName{Name: npName}, - }) +// decodePoolAttrs resolves nodepool attributes from node annotation +func decodePoolAttrs(node *corev1.Node) (*NodePoolRelatedAttributes, error) { + var oldNpra NodePoolRelatedAttributes + if preAttrs, exist := node.Annotations[apps.AnnotationPrevAttrs]; !exist { + return &oldNpra, nil + } else { + if err := json.Unmarshal([]byte(preAttrs), &oldNpra); err != nil { + return &oldNpra, err + } + + return &oldNpra, nil + } +} + +// areNodePoolRelatedAttributesEqual is used for checking NodePoolRelatedAttributes is equal +func areNodePoolRelatedAttributesEqual(a, b *NodePoolRelatedAttributes) bool { + if a == nil || b == nil { + return a == b + } + + isLabelsEqual := (len(a.Labels) == 0 && len(b.Labels) == 0) || reflect.DeepEqual(a.Labels, b.Labels) + isAnnotationsEqual := (len(a.Annotations) == 0 && len(b.Annotations) == 0) || reflect.DeepEqual(a.Annotations, b.Annotations) + isTaintsEqual := (len(a.Taints) == 0 && len(b.Taints) == 0) || reflect.DeepEqual(a.Taints, b.Taints) + + return isLabelsEqual && isAnnotationsEqual && isTaintsEqual } diff --git a/pkg/controller/nodepool/util_test.go b/pkg/controller/nodepool/util_test.go new file mode 100644 index 00000000000..8afb6065506 --- /dev/null +++ b/pkg/controller/nodepool/util_test.go @@ -0,0 +1,859 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nodepool + +import ( + "encoding/json" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openyurtio/openyurt/pkg/apis/apps" + appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" +) + +func TestConcilateNode(t *testing.T) { + testcases := map[string]struct { + initNpra *NodePoolRelatedAttributes + mockNode corev1.Node + pool appsv1beta1.NodePool + wantedNodeExcludeAttribute corev1.Node + updated bool + }{ + "node has no pool attributes": { + mockNode: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + pool: appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Labels: map[string]string{ + "poollabel1": "value1", + "poollabel2": "value2", + }, + Annotations: map[string]string{ + "poolanno1": "value1", + "poolanno2": "value2", + }, + Taints: []corev1.Taint{ + { + Key: "poolkey1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + wantedNodeExcludeAttribute: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + "poollabel1": "value1", + "poollabel2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + "poolanno1": "value1", + "poolanno2": "value2", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "poolkey1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + updated: true, + }, + "node has some pool attributes": { + initNpra: &NodePoolRelatedAttributes{ + Labels: map[string]string{ + "label2": "value2", + }, + Annotations: map[string]string{ + "anno2": "value2", + "anno3": "value3", + }, + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + mockNode: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + "anno2": "value2", + "anno3": "value3", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key2", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key3", + Effect: corev1.TaintEffectNoExecute, + }, + }, + }, + }, + pool: appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Labels: map[string]string{ + "poollabel1": "value1", + "poollabel2": "value2", + }, + Annotations: map[string]string{ + "poolanno1": "value1", + "poolanno2": "value2", + }, + Taints: []corev1.Taint{ + { + Key: "poolkey1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + wantedNodeExcludeAttribute: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "poollabel1": "value1", + "poollabel2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + "poolanno1": "value1", + "poolanno2": "value2", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key2", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key3", + Effect: corev1.TaintEffectNoExecute, + }, + { + Key: "poolkey1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + updated: true, + }, + "pool attributes is not changed": { + initNpra: &NodePoolRelatedAttributes{ + Labels: map[string]string{ + "label2": "value2", + }, + Annotations: map[string]string{ + "anno2": "value2", + "anno3": "value3", + }, + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + mockNode: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + "anno2": "value2", + "anno3": "value3", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key2", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key3", + Effect: corev1.TaintEffectNoExecute, + }, + }, + }, + }, + pool: appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Labels: map[string]string{ + "label2": "value2", + }, + Annotations: map[string]string{ + "anno2": "value2", + "anno3": "value3", + }, + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + wantedNodeExcludeAttribute: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + "anno2": "value2", + "anno3": "value3", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key2", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key3", + Effect: corev1.TaintEffectNoExecute, + }, + }, + }, + }, + updated: false, + }, + "pool has some duplicated attributes": { + mockNode: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + "anno2": "value2", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + pool: appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Labels: map[string]string{ + "label2": "value2", + "poollabel2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + }, + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + wantedNodeExcludeAttribute: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + "poollabel2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + "anno2": "value2", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + updated: true, + }, + "node and pool has no pool attributes": { + mockNode: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + pool: appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Labels: map[string]string{}, + Annotations: map[string]string{}, + Taints: []corev1.Taint{}, + }, + }, + wantedNodeExcludeAttribute: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + Annotations: map[string]string{ + "anno1": "value1", + }, + }, + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + }, + updated: false, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + if tc.initNpra != nil { + if err := encodePoolAttrs(&tc.mockNode, tc.initNpra); err != nil { + t.Errorf("Expected no error, got %v", err) + } + } + + changed, err := conciliateNode(&tc.mockNode, &tc.pool) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if tc.updated != changed { + t.Errorf("Expected %v, got %v, node %#+v", tc.updated, changed, tc.mockNode) + } + + wantedNpra := NodePoolRelatedAttributes{ + Labels: tc.pool.Spec.Labels, + Annotations: tc.pool.Spec.Annotations, + Taints: tc.pool.Spec.Taints, + } + npra, err := decodePoolAttrs(&tc.mockNode) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if !areNodePoolRelatedAttributesEqual(npra, &wantedNpra) { + t.Errorf("Expected %v, got %v", wantedNpra, *npra) + } + + delete(tc.mockNode.Annotations, apps.AnnotationPrevAttrs) + if !reflect.DeepEqual(tc.mockNode, tc.wantedNodeExcludeAttribute) { + t.Errorf("Expected %v, got %v", tc.wantedNodeExcludeAttribute, tc.mockNode) + } + }) + } +} + +func TestConciliateLabels(t *testing.T) { + mockNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label1": "value1", + "label2": "value2", + }, + }, + } + + oldLabels := map[string]string{ + "label1": "value1", + } + newLabels := map[string]string{ + "label3": "value3", + "label4": "value4", + } + + wantNodeLabels := map[string]string{ + "label2": "value2", + "label3": "value3", + "label4": "value4", + } + + conciliateLabels(mockNode, oldLabels, newLabels) + if !reflect.DeepEqual(wantNodeLabels, mockNode.Labels) { + t.Errorf("Expected %v, got %v", wantNodeLabels, mockNode.Labels) + } +} + +func TestConciliateAnnotations(t *testing.T) { + mockNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "anno1": "value1", + "anno2": "value2", + }, + }, + } + + oldAnnos := map[string]string{ + "anno1": "value1", + } + newAnnos := map[string]string{ + "anno3": "value3", + "anno4": "value4", + } + + wantNodeAnnos := map[string]string{ + "anno2": "value2", + "anno3": "value3", + "anno4": "value4", + } + + conciliateAnnotations(mockNode, oldAnnos, newAnnos) + if !reflect.DeepEqual(wantNodeAnnos, mockNode.Annotations) { + t.Errorf("Expected %v, got %v", wantNodeAnnos, mockNode.Annotations) + } +} + +func TestConciliateTaints(t *testing.T) { + mockNode := &corev1.Node{ + Spec: corev1.NodeSpec{ + Taints: []corev1.Taint{ + { + Key: "key1", + Value: "value1", + Effect: corev1.TaintEffectNoExecute, + }, + { + Key: "key2", + Value: "value2", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + } + + // Test case where oldTaint is present in the node and should be removed + oldTaints := []corev1.Taint{ + { + Key: "key1", + Value: "value1", + Effect: corev1.TaintEffectNoExecute, + }, + } + newTaints := []corev1.Taint{ + { + Key: "key3", + Value: "value3", + Effect: corev1.TaintEffectPreferNoSchedule, + }, + } + wantNodeTaints := []corev1.Taint{ + { + Key: "key2", + Value: "value2", + Effect: corev1.TaintEffectNoSchedule, + }, + { + Key: "key3", + Value: "value3", + Effect: corev1.TaintEffectPreferNoSchedule, + }, + } + conciliateTaints(mockNode, oldTaints, newTaints) + + if !reflect.DeepEqual(wantNodeTaints, mockNode.Spec.Taints) { + t.Errorf("Expected %v, got %v", wantNodeTaints, mockNode.Spec.Taints) + } +} + +func TestConciliateNodePoolStatus(t *testing.T) { + testcases := map[string]struct { + readyNodes int32 + notReadyNodes int32 + nodes []string + pool *appsv1beta1.NodePool + needUpdated bool + }{ + "status is needed to update": { + readyNodes: 5, + notReadyNodes: 2, + nodes: []string{"foo", "bar", "cat", "zxxde"}, + pool: &appsv1beta1.NodePool{ + Status: appsv1beta1.NodePoolStatus{ + ReadyNodeNum: 2, + UnreadyNodeNum: 3, + Nodes: []string{"foo", "bar", "cat", "zxxde", "lucky"}, + }, + }, + needUpdated: true, + }, + "status is not updated": { + readyNodes: 2, + notReadyNodes: 2, + nodes: []string{"foo", "bar", "cat", "zxxde"}, + pool: &appsv1beta1.NodePool{ + Status: appsv1beta1.NodePoolStatus{ + ReadyNodeNum: 2, + UnreadyNodeNum: 2, + Nodes: []string{"foo", "bar", "cat", "zxxde"}, + }, + }, + needUpdated: false, + }, + "status is not updated when pool is empty": { + readyNodes: 0, + notReadyNodes: 0, + nodes: []string{}, + pool: &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Status: appsv1beta1.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + needUpdated: false, + }, + "status is not updated when pool has no status": { + readyNodes: 0, + notReadyNodes: 0, + nodes: []string{}, + pool: &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + }, + needUpdated: false, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + needUpdated := conciliateNodePoolStatus(tc.readyNodes, tc.notReadyNodes, tc.nodes, tc.pool) + if needUpdated != tc.needUpdated { + t.Errorf("Expected %v, got %v", tc.needUpdated, needUpdated) + } + }) + } +} + +func TestContainTaint(t *testing.T) { + mockTaints := []corev1.Taint{ + { + Key: "key1", + Value: "value1", + Effect: corev1.TaintEffectNoExecute, + }, + { + Key: "key2", + Value: "value2", + Effect: corev1.TaintEffectNoSchedule, + }, + } + testcases := map[string]struct { + inputTaint corev1.Taint + resultIndex int + isContained bool + }{ + "taint is contained": { + inputTaint: corev1.Taint{ + Key: "key1", + Value: "value1", + Effect: corev1.TaintEffectNoExecute, + }, + resultIndex: 0, + isContained: true, + }, + "taint is not contained": { + inputTaint: corev1.Taint{ + Key: "key3", + Value: "value3", + Effect: corev1.TaintEffectPreferNoSchedule, + }, + resultIndex: 0, + isContained: false, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + gotIndex, gotBool := containTaint(tc.inputTaint, mockTaints) + if gotIndex != tc.resultIndex || gotBool != tc.isContained { + t.Errorf("Expected index %v and bool %v, got index %v and bool %v", tc.resultIndex, tc.isContained, gotIndex, gotBool) + } + }) + } +} + +func TestRemoveTaint(t *testing.T) { + mockTaints := []corev1.Taint{ + { + Key: "key1", + Value: "value1", + Effect: corev1.TaintEffectNoExecute, + }, + { + Key: "key2", + Value: "value2", + Effect: corev1.TaintEffectNoSchedule, + }, + } + + testcases := map[string]struct { + mockTaint corev1.Taint + wantTaints []corev1.Taint + }{ + "remove exist taint": { + mockTaint: corev1.Taint{ + Key: "key1", + Value: "value1", + Effect: corev1.TaintEffectNoExecute, + }, + wantTaints: []corev1.Taint{ + { + Key: "key2", + Value: "value2", + Effect: corev1.TaintEffectNoSchedule, + }, + }, + }, + "remove not exist taint": { + mockTaint: corev1.Taint{ + Key: "key3", + Value: "value3", + Effect: corev1.TaintEffectPreferNoSchedule, + }, + wantTaints: mockTaints, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + gotTaints := removeTaint(tc.mockTaint, mockTaints) + if !reflect.DeepEqual(tc.wantTaints, gotTaints) { + t.Errorf("Expected %v, got %v", tc.wantTaints, gotTaints) + } + }) + } +} + +func TestEncodePoolAttrs(t *testing.T) { + testcases := map[string]struct { + mockNode *corev1.Node + mockNpra *NodePoolRelatedAttributes + }{ + "annotations is not set": { + mockNode: &corev1.Node{}, + mockNpra: &NodePoolRelatedAttributes{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + "annotations is set": { + mockNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "foo": "bar", + }, + }, + }, + mockNpra: &NodePoolRelatedAttributes{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + gotErr := encodePoolAttrs(tc.mockNode, tc.mockNpra) + if gotErr != nil { + t.Errorf("Expected no error, got %v", gotErr) + } + + // Ensure that the NodePoolRelatedAttributes has been correctly stored in the node's annotations + gotNpra, err := decodePoolAttrs(tc.mockNode) + if err != nil || !reflect.DeepEqual(gotNpra, tc.mockNpra) { + t.Errorf("Expected %v, got %v", tc.mockNpra, gotNpra) + } + }) + } +} + +func TestDecodePoolAttrs(t *testing.T) { + wantNpra := &NodePoolRelatedAttributes{ + Labels: map[string]string{ + "foo": "bar", + }, + } + npraJson, err := json.Marshal(wantNpra) + if err != nil { + t.Errorf("failed to marshal npra") + } + + mockNode := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + apps.AnnotationPrevAttrs: string(npraJson), + }, + }, + } + gotNpra, gotErr := decodePoolAttrs(mockNode) + + if gotErr != nil { + t.Errorf("Expected no error, got %v", gotErr) + } + + if !reflect.DeepEqual(wantNpra, gotNpra) { + t.Errorf("Expected %v, got %v", wantNpra, gotNpra) + } +} + +func TestIsNodeReady(t *testing.T) { + tests := []struct { + name string + node corev1.Node + want bool + }{ + { + name: "NodeReady and ConditionTrue", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + want: true, + }, + { + name: "NodeReady but ConditionFalse", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionFalse, + }, + }, + }, + }, + want: false, + }, + { + name: "Node status not NodeReady", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeMemoryPressure, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isNodeReady(tt.node); got != tt.want { + t.Errorf("isNodeReady() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/webhook/node/v1/node_default.go b/pkg/webhook/node/v1/node_default.go new file mode 100644 index 00000000000..ed352367a67 --- /dev/null +++ b/pkg/webhook/node/v1/node_default.go @@ -0,0 +1,61 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "fmt" + "strings" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/openyurtio/openyurt/pkg/apis/apps" + appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" +) + +// Default satisfies the defaulting webhook interface. +func (webhook *NodeHandler) Default(ctx context.Context, obj runtime.Object, req admission.Request) error { + node, ok := obj.(*v1.Node) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a Node but got a %T", obj)) + } + + npName, ok := node.Labels[apps.NodePoolLabel] + if !ok { + return nil + } + + var np appsv1beta1.NodePool + if err := webhook.Client.Get(ctx, types.NamespacedName{Name: npName}, &np); err != nil { + return err + } + + // add NodePool.Spec.Type to node labels + if node.Labels == nil { + node.Labels = make(map[string]string) + } + node.Labels[apps.NodePoolTypeLabel] = strings.ToLower(string(np.Spec.Type)) + + if np.Spec.HostNetwork { + node.Labels[apps.NodePoolHostNetworkLabel] = "true" + } + return nil +} diff --git a/pkg/webhook/node/v1/node_default_test.go b/pkg/webhook/node/v1/node_default_test.go new file mode 100644 index 00000000000..76d0679bf3c --- /dev/null +++ b/pkg/webhook/node/v1/node_default_test.go @@ -0,0 +1,125 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "net/http" + "strings" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/apis/apps" + appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" +) + +func TestDefault(t *testing.T) { + testcases := map[string]struct { + node runtime.Object + pool *appsv1beta1.NodePool + errCode int + errMsg string + }{ + "it is not a node": { + node: &corev1.Pod{}, + errCode: http.StatusBadRequest, + }, + "it is a orphan node": { + node: &corev1.Node{}, + errCode: 0, + }, + "nodepool doesn't exist": { + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + }, + }, + }, + errCode: 0, + errMsg: "not found", + }, + "add labels for node": { + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + apps.NodePoolLabel: "shanghai", + }, + }, + }, + pool: &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "shanghai", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + HostNetwork: true, + }, + }, + errCode: 0, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatal("Fail to add kubernetes clint-go custom resource") + } + apis.AddToScheme(scheme) + + var c client.Client + if tc.pool != nil { + c = fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(tc.pool).Build() + } else { + c = fakeclient.NewClientBuilder().WithScheme(scheme).Build() + } + + w := &NodeHandler{Client: c} + err := w.Default(context.TODO(), tc.node, admission.Request{}) + if err != nil { + if tc.errCode != 0 { + statusErr := err.(*errors.StatusError) + if tc.errCode != int(statusErr.Status().Code) { + t.Errorf("Expected error code %d, got %v", tc.errCode, err) + return + } + } + + if len(tc.errMsg) != 0 { + if !strings.Contains(err.Error(), tc.errMsg) { + t.Errorf("Expected error msg %s, got %s", tc.errMsg, err.Error()) + return + } + } + } else if tc.errCode != 0 && len(tc.errMsg) != 0 { + t.Errorf("Expected error code %d, errmsg %s, got %v", tc.errCode, tc.errMsg, err) + } + }) + } +} diff --git a/pkg/webhook/node/v1/node_handler.go b/pkg/webhook/node/v1/node_handler.go index bb107988c50..f6cbc5b6aa0 100644 --- a/pkg/webhook/node/v1/node_handler.go +++ b/pkg/webhook/node/v1/node_handler.go @@ -30,7 +30,7 @@ const ( WebhookName = "node" ) -// SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error +// SetupWebhookWithManager sets up Cluster webhooks. mutate path, validate path, error func (webhook *NodeHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { // init webhook.Client = mgr.GetClient() @@ -43,15 +43,18 @@ func (webhook *NodeHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, s util.GenerateValidatePath(gvk), builder.WebhookManagedBy(mgr). For(&v1.Node{}). + WithDefaulter(webhook). WithValidator(webhook). Complete() } -// +kubebuilder:webhook:path=/validate-core-openyurt-io-v1-node,mutating=false,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1;v1beta1,groups="",resources=nodes,verbs=update,versions=v1,name=validate.core.v1.node.openyurt.io +// +kubebuilder:webhook:path=/validate-core-openyurt-io-v1-node,mutating=false,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1,groups="",resources=nodes,verbs=update,versions=v1,name=validate.core.v1.node.openyurt.io +// +kubebuilder:webhook:path=/mutate-core-openyurt-io-v1-node,mutating=true,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1,groups="",resources=nodes,verbs=create;update,versions=v1,name=mutate.core.v1.node.openyurt.io -// Cluster implements a validating and defaulting webhook for Cluster. +// NodeHandler implements a validating and defaulting webhook for Cluster. type NodeHandler struct { Client client.Client } +var _ builder.CustomDefaulter = &NodeHandler{} var _ builder.CustomValidator = &NodeHandler{} diff --git a/pkg/webhook/node/v1/node_validation.go b/pkg/webhook/node/v1/node_validation.go index cfc6a0cba61..7ee9f6c1e80 100644 --- a/pkg/webhook/node/v1/node_validation.go +++ b/pkg/webhook/node/v1/node_validation.go @@ -58,21 +58,31 @@ func (webhook *NodeHandler) ValidateDelete(_ context.Context, obj runtime.Object } func validateNodeUpdate(newNode, oldNode *v1.Node, req admission.Request) field.ErrorList { - oldNp := oldNode.Labels[apps.LabelDesiredNodePool] - newNp := newNode.Labels[apps.LabelDesiredNodePool] + oldNp := oldNode.Labels[apps.NodePoolLabel] + newNp := newNode.Labels[apps.NodePoolLabel] + oldNpType := oldNode.Labels[apps.NodePoolTypeLabel] + newNpType := newNode.Labels[apps.NodePoolTypeLabel] + oldNpHostNetwork := oldNode.Labels[apps.NodePoolHostNetworkLabel] + newNpHostNetwork := newNode.Labels[apps.NodePoolHostNetworkLabel] + + var errList field.ErrorList + // it is not allowed to change NodePoolLabel if it has been set + if len(oldNp) != 0 && oldNp != newNp { + errList = append(errList, field.Forbidden(field.NewPath("metadata").Child("labels").Child(apps.NodePoolLabel), "apps.openyurt.io/nodepool can not be changed")) + } - if len(oldNp) == 0 { - return nil + // it is not allowed to change NodePoolTypeLabel if it has been set + if len(oldNpType) != 0 && oldNpType != newNpType { + errList = append(errList, field.Forbidden(field.NewPath("metadata").Child("labels").Child(apps.NodePoolTypeLabel), "nodepool.openyurt.io/type can not be changed")) } - // can not change LabelDesiredNodePool if it has been set - if oldNp != newNp { - return field.ErrorList([]*field.Error{ - field.Forbidden( - field.NewPath("metadata").Child("labels").Child(apps.LabelDesiredNodePool), - "apps.openyurt.io/desired-nodepool can not be changed"), - }) + // it is not allowed to change NodePoolHostNetworkLabel if it has been set + if len(oldNpHostNetwork) != 0 && oldNpHostNetwork != newNpHostNetwork { + errList = append(errList, field.Forbidden(field.NewPath("metadata").Child("labels").Child(apps.NodePoolHostNetworkLabel), "nodepool.openyurt.io/hostnetwork can not be changed")) } + if len(errList) != 0 { + return errList + } return nil } diff --git a/pkg/webhook/node/v1/node_validation_test.go b/pkg/webhook/node/v1/node_validation_test.go new file mode 100644 index 00000000000..6aa88d3d0e8 --- /dev/null +++ b/pkg/webhook/node/v1/node_validation_test.go @@ -0,0 +1,156 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "net/http" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/openyurtio/openyurt/pkg/apis/apps" +) + +func TestValidateUpdate(t *testing.T) { + testcases := map[string]struct { + oldNode runtime.Object + newNode runtime.Object + errCode int + }{ + "old object is not a node": { + oldNode: &corev1.Pod{}, + newNode: &corev1.Node{}, + errCode: http.StatusBadRequest, + }, + "new object is not a node": { + oldNode: &corev1.Node{}, + newNode: &corev1.Pod{}, + errCode: http.StatusBadRequest, + }, + "node pool is changed": { + oldNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + }, + }, + }, + newNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "shanghai", + }, + }, + }, + errCode: http.StatusUnprocessableEntity, + }, + "node pool type is changed": { + oldNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + apps.NodePoolTypeLabel: "edge", + }, + }, + }, + newNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + apps.NodePoolTypeLabel: "cloud", + }, + }, + }, + errCode: http.StatusUnprocessableEntity, + }, + "node pool host network is changed": { + oldNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + apps.NodePoolTypeLabel: "edge", + apps.NodePoolHostNetworkLabel: "true", + }, + }, + }, + newNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + apps.NodePoolTypeLabel: "edge", + apps.NodePoolHostNetworkLabel: "false", + }, + }, + }, + errCode: http.StatusUnprocessableEntity, + }, + "it is a normal node update": { + oldNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + apps.NodePoolTypeLabel: "edge", + apps.NodePoolHostNetworkLabel: "true", + }, + }, + }, + newNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + apps.NodePoolTypeLabel: "edge", + apps.NodePoolHostNetworkLabel: "true", + }, + }, + }, + errCode: 0, + }, + "it is a normal node update without init labels": { + oldNode: &corev1.Node{}, + newNode: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + apps.NodePoolTypeLabel: "edge", + apps.NodePoolHostNetworkLabel: "true", + }, + }, + }, + errCode: 0, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + h := &NodeHandler{} + err := h.ValidateUpdate(context.TODO(), tc.oldNode, tc.newNode, admission.Request{}) + if tc.errCode == 0 && err != nil { + t.Errorf("Expected error code %d, got %v", tc.errCode, err) + } else if tc.errCode != 0 { + statusErr := err.(*errors.StatusError) + if tc.errCode != int(statusErr.Status().Code) { + t.Errorf("Expected error code %d, got %v", tc.errCode, err) + } + } + }) + } +} diff --git a/pkg/webhook/nodepool/v1alpha1/nodepool_default.go b/pkg/webhook/nodepool/v1alpha1/nodepool_default.go deleted file mode 100644 index bc0887736f3..00000000000 --- a/pkg/webhook/nodepool/v1alpha1/nodepool_default.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - "context" - "fmt" - "strings" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - - "github.com/openyurtio/openyurt/pkg/apis/apps" - "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" -) - -// Default satisfies the defaulting webhook interface. -func (webhook *NodePoolHandler) Default(ctx context.Context, obj runtime.Object) error { - np, ok := obj.(*v1alpha1.NodePool) - if !ok { - return apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", obj)) - } - - np.Spec.Selector = &metav1.LabelSelector{ - MatchLabels: map[string]string{apps.LabelCurrentNodePool: np.Name}, - } - - // add NodePool.Spec.Type to NodePool labels - if np.Labels == nil { - np.Labels = make(map[string]string) - } - np.Labels[apps.NodePoolTypeLabelKey] = strings.ToLower(string(np.Spec.Type)) - - return nil -} diff --git a/pkg/webhook/nodepool/v1alpha1/nodepool_handler.go b/pkg/webhook/nodepool/v1alpha1/nodepool_handler.go deleted file mode 100644 index bad4db5540e..00000000000 --- a/pkg/webhook/nodepool/v1alpha1/nodepool_handler.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - "sigs.k8s.io/controller-runtime/pkg/webhook" - - appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/webhook/util" -) - -// SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error -func (webhook *NodePoolHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { - // init - webhook.Client = mgr.GetClient() - - gvk, err := apiutil.GVKForObject(&appsv1alpha1.NodePool{}, mgr.GetScheme()) - if err != nil { - return "", "", err - } - return util.GenerateMutatePath(gvk), - util.GenerateValidatePath(gvk), - ctrl.NewWebhookManagedBy(mgr). - For(&appsv1alpha1.NodePool{}). - Complete() -} - -// NodePoolHandler implements a validating and defaulting webhook for Cluster. -type NodePoolHandler struct { - Client client.Client -} - -var _ webhook.CustomDefaulter = &NodePoolHandler{} -var _ webhook.CustomValidator = &NodePoolHandler{} diff --git a/pkg/webhook/nodepool/v1alpha1/nodepool_validation.go b/pkg/webhook/nodepool/v1alpha1/nodepool_validation.go deleted file mode 100644 index 383e321844e..00000000000 --- a/pkg/webhook/nodepool/v1alpha1/nodepool_validation.go +++ /dev/null @@ -1,143 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v1alpha1 - -import ( - "context" - "errors" - "fmt" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - apivalidation "k8s.io/apimachinery/pkg/api/validation" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/openyurtio/openyurt/pkg/apis/apps" - appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" -) - -// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. -func (webhook *NodePoolHandler) ValidateCreate(ctx context.Context, obj runtime.Object) error { - np, ok := obj.(*appsv1alpha1.NodePool) - if !ok { - return apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", obj)) - } - - if allErrs := validateNodePoolSpec(&np.Spec); len(allErrs) > 0 { - return apierrors.NewInvalid(appsv1alpha1.GroupVersion.WithKind("NodePool").GroupKind(), np.Name, allErrs) - } - - return nil -} - -// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. -func (webhook *NodePoolHandler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error { - newNp, ok := newObj.(*appsv1alpha1.NodePool) - if !ok { - return apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", newObj)) - } - oldNp, ok := oldObj.(*appsv1alpha1.NodePool) - if !ok { - return apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", oldObj)) - } - - if allErrs := validateNodePoolSpecUpdate(&newNp.Spec, &oldNp.Spec); len(allErrs) > 0 { - return apierrors.NewInvalid(appsv1alpha1.GroupVersion.WithKind("NodePool").GroupKind(), newNp.Name, allErrs) - } - - return nil -} - -// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. -func (webhook *NodePoolHandler) ValidateDelete(_ context.Context, obj runtime.Object) error { - np, ok := obj.(*appsv1alpha1.NodePool) - if !ok { - return apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", obj)) - } - if allErrs := validateNodePoolDeletion(webhook.Client, np); len(allErrs) > 0 { - return apierrors.NewInvalid(appsv1alpha1.GroupVersion.WithKind("NodePool").GroupKind(), np.Name, allErrs) - } - - return nil -} - -// annotationValidator validates the NodePool.Spec.Annotations -var annotationValidator = func(annos map[string]string) error { - errs := apivalidation.ValidateAnnotations(annos, field.NewPath("field")) - if len(errs) > 0 { - return errors.New(errs.ToAggregate().Error()) - } - return nil -} - -func validateNodePoolSpecAnnotations(annotations map[string]string) field.ErrorList { - if err := annotationValidator(annotations); err != nil { - return field.ErrorList([]*field.Error{ - field.Invalid(field.NewPath("spec").Child("annotations"), - annotations, "invalid annotations")}) - } - return nil -} - -// validateNodePoolSpec validates the nodepool spec. -func validateNodePoolSpec(spec *appsv1alpha1.NodePoolSpec) field.ErrorList { - if allErrs := validateNodePoolSpecAnnotations(spec.Annotations); allErrs != nil { - return allErrs - } - return nil -} - -// validateNodePoolSpecUpdate tests if required fields in the NodePool spec are set. -func validateNodePoolSpecUpdate(spec, oldSpec *appsv1alpha1.NodePoolSpec) field.ErrorList { - if allErrs := validateNodePoolSpec(spec); allErrs != nil { - return allErrs - } - - if spec.Type != oldSpec.Type { - return field.ErrorList([]*field.Error{ - field.Invalid(field.NewPath("spec").Child("type"), - spec.Annotations, "pool type can't be changed")}) - } - return nil -} - -// validateNodePoolDeletion validate the nodepool deletion event, which prevents -// the default-nodepool from being deleted -func validateNodePoolDeletion(cli client.Client, np *appsv1alpha1.NodePool) field.ErrorList { - nodes := corev1.NodeList{} - - if np.Name == apps.DefaultCloudNodePoolName || np.Name == apps.DefaultEdgeNodePoolName { - return field.ErrorList([]*field.Error{ - field.Forbidden(field.NewPath("metadata").Child("name"), - fmt.Sprintf("default nodepool %s forbidden to delete", np.Name))}) - } - - if err := cli.List(context.TODO(), &nodes, - client.MatchingLabels(np.Spec.Selector.MatchLabels)); err != nil { - return field.ErrorList([]*field.Error{ - field.Forbidden(field.NewPath("metadata").Child("name"), - "fail to get nodes associated to the pool")}) - } - if len(nodes.Items) != 0 { - return field.ErrorList([]*field.Error{ - field.Forbidden(field.NewPath("metadata").Child("name"), - "cannot remove nonempty pool, please drain the pool before deleting")}) - } - return nil -} diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_default.go b/pkg/webhook/nodepool/v1beta1/nodepool_default.go index bf05d2f0a65..cd3c081f439 100644 --- a/pkg/webhook/nodepool/v1beta1/nodepool_default.go +++ b/pkg/webhook/nodepool/v1beta1/nodepool_default.go @@ -19,13 +19,10 @@ package v1beta1 import ( "context" "fmt" - "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "github.com/openyurtio/openyurt/pkg/apis/apps" "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" ) @@ -36,15 +33,17 @@ func (webhook *NodePoolHandler) Default(ctx context.Context, obj runtime.Object) return apierrors.NewBadRequest(fmt.Sprintf("expected a NodePool but got a %T", obj)) } - np.Spec.Selector = &metav1.LabelSelector{ - MatchLabels: map[string]string{apps.LabelCurrentNodePool: np.Name}, + // specify default type as Edge + if len(np.Spec.Type) == 0 { + np.Spec.Type = v1beta1.Edge } - // add NodePool.Spec.Type to NodePool labels - if np.Labels == nil { - np.Labels = make(map[string]string) + // init node pool status + np.Status = v1beta1.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: make([]string, 0), } - np.Labels[apps.NodePoolTypeLabelKey] = strings.ToLower(string(np.Spec.Type)) return nil } diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_default_test.go b/pkg/webhook/nodepool/v1beta1/nodepool_default_test.go new file mode 100644 index 00000000000..100bbe840a1 --- /dev/null +++ b/pkg/webhook/nodepool/v1beta1/nodepool_default_test.go @@ -0,0 +1,110 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "context" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" +) + +func TestDefault(t *testing.T) { + testcases := map[string]struct { + obj runtime.Object + errHappened bool + wantedNodePool *v1beta1.NodePool + }{ + "it is not a nodepool": { + obj: &corev1.Pod{}, + errHappened: true, + }, + "nodepool has no type": { + obj: &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: v1beta1.NodePoolSpec{ + HostNetwork: true, + }, + }, + wantedNodePool: &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: v1beta1.NodePoolSpec{ + HostNetwork: true, + Type: v1beta1.Edge, + }, + Status: v1beta1.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + }, + "nodepool has pool type": { + obj: &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: v1beta1.NodePoolSpec{ + HostNetwork: true, + Type: v1beta1.Cloud, + }, + }, + wantedNodePool: &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: v1beta1.NodePoolSpec{ + HostNetwork: true, + Type: v1beta1.Cloud, + }, + Status: v1beta1.NodePoolStatus{ + ReadyNodeNum: 0, + UnreadyNodeNum: 0, + Nodes: []string{}, + }, + }, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + h := NodePoolHandler{} + err := h.Default(context.TODO(), tc.obj) + if tc.errHappened { + if err == nil { + t.Errorf("expect error, got nil") + } + } else if err != nil { + t.Errorf("expect no error, but got %v", err) + } else { + currentNp := tc.obj.(*v1beta1.NodePool) + if !reflect.DeepEqual(currentNp, tc.wantedNodePool) { + t.Errorf("expect %#+v, got %#+v", tc.wantedNodePool, currentNp) + } + } + }) + } +} diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_handler.go b/pkg/webhook/nodepool/v1beta1/nodepool_handler.go index de606d8988c..d1fa643d5f7 100644 --- a/pkg/webhook/nodepool/v1beta1/nodepool_handler.go +++ b/pkg/webhook/nodepool/v1beta1/nodepool_handler.go @@ -44,8 +44,8 @@ func (webhook *NodePoolHandler) SetupWebhookWithManager(mgr ctrl.Manager) (strin Complete() } -// +kubebuilder:webhook:verbs=create;update;delete,path=/validate-apps-openyurt-io-v1beta1-nodepool,mutating=false,failurePolicy=fail,groups=apps.openyurt.io,resources=nodepools,versions=v1beta1,name=v.v1beta1.nodepool.kb.io,sideEffects=None,admissionReviewVersions=v1 -// +kubebuilder:webhook:path=/mutate-apps-openyurt-io-v1beta1-nodepool,mutating=true,failurePolicy=fail,groups=apps.openyurt.io,resources=nodepools,verbs=create;update,versions=v1beta1,name=m.v1beta1.nodepool.kb.io,sideEffects=None,admissionReviewVersions=v1 +// +kubebuilder:webhook:path=/validate-apps-openyurt-io-v1beta1-nodepool,mutating=false,failurePolicy=fail,groups=apps.openyurt.io,resources=nodepools,verbs=create;update;delete,versions=v1beta1,name=v.v1beta1.nodepool.kb.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:path=/mutate-apps-openyurt-io-v1beta1-nodepool,mutating=true,failurePolicy=fail,groups=apps.openyurt.io,resources=nodepools,verbs=create,versions=v1beta1,name=m.v1beta1.nodepool.kb.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 // NodePoolHandler implements a validating and defaulting webhook for Cluster. type NodePoolHandler struct { diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_validation.go b/pkg/webhook/nodepool/v1beta1/nodepool_validation.go index a7475b87697..81f7959d1a6 100644 --- a/pkg/webhook/nodepool/v1beta1/nodepool_validation.go +++ b/pkg/webhook/nodepool/v1beta1/nodepool_validation.go @@ -100,6 +100,11 @@ func validateNodePoolSpec(spec *appsv1beta1.NodePoolSpec) field.ErrorList { if allErrs := validateNodePoolSpecAnnotations(spec.Annotations); allErrs != nil { return allErrs } + + // NodePool type should be Edge or Cloud + if spec.Type != appsv1beta1.Edge && spec.Type != appsv1beta1.Cloud { + return []*field.Error{field.Invalid(field.NewPath("spec").Child("type"), spec.Type, "pool type should be Edge or Cloud")} + } return nil } @@ -111,8 +116,13 @@ func validateNodePoolSpecUpdate(spec, oldSpec *appsv1beta1.NodePoolSpec) field.E if spec.Type != oldSpec.Type { return field.ErrorList([]*field.Error{ - field.Invalid(field.NewPath("spec").Child("type"), - spec.Annotations, "pool type can't be changed")}) + field.Invalid(field.NewPath("spec").Child("type"), spec.Type, "pool type can't be changed")}) + } + + if spec.HostNetwork != oldSpec.HostNetwork { + return field.ErrorList([]*field.Error{ + field.Invalid(field.NewPath("spec").Child("hostNetwork"), spec.HostNetwork, "pool hostNetwork can't be changed"), + }) } return nil } @@ -122,14 +132,7 @@ func validateNodePoolSpecUpdate(spec, oldSpec *appsv1beta1.NodePoolSpec) field.E func validateNodePoolDeletion(cli client.Client, np *appsv1beta1.NodePool) field.ErrorList { nodes := corev1.NodeList{} - if np.Name == apps.DefaultCloudNodePoolName || np.Name == apps.DefaultEdgeNodePoolName { - return field.ErrorList([]*field.Error{ - field.Forbidden(field.NewPath("metadata").Child("name"), - fmt.Sprintf("default nodepool %s forbidden to delete", np.Name))}) - } - - if err := cli.List(context.TODO(), &nodes, - client.MatchingLabels(np.Spec.Selector.MatchLabels)); err != nil { + if err := cli.List(context.TODO(), &nodes, client.MatchingLabels(map[string]string{apps.NodePoolLabel: np.Name})); err != nil { return field.ErrorList([]*field.Error{ field.Forbidden(field.NewPath("metadata").Child("name"), "fail to get nodes associated to the pool")}) diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_validation_test.go b/pkg/webhook/nodepool/v1beta1/nodepool_validation_test.go new file mode 100644 index 00000000000..0305d703225 --- /dev/null +++ b/pkg/webhook/nodepool/v1beta1/nodepool_validation_test.go @@ -0,0 +1,285 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "context" + "net/http" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/apis/apps" + appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" +) + +func TestValidateCreate(t *testing.T) { + testcases := map[string]struct { + pool runtime.Object + errcode int + }{ + "it is a normal nodepool": { + pool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + }, + }, + errcode: 0, + }, + "it is not a nodepool": { + pool: &corev1.Node{}, + errcode: http.StatusBadRequest, + }, + "invalid annotation": { + pool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Annotations: map[string]string{ + "-&#foo": "invalid annotation", + }, + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + "invalid pool type": { + pool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: "invalid type", + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + } + + handler := &NodePoolHandler{} + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + err := handler.ValidateCreate(context.TODO(), tc.pool) + if tc.errcode == 0 && err != nil { + t.Errorf("Expected error code %d, got %v", tc.errcode, err) + } else if tc.errcode != 0 { + statusErr := err.(*errors.StatusError) + if tc.errcode != int(statusErr.Status().Code) { + t.Errorf("Expected error code %d, got %v", tc.errcode, err) + } + } + }) + } +} + +func TestValidateUpdate(t *testing.T) { + testcases := map[string]struct { + oldPool runtime.Object + newPool runtime.Object + errcode int + }{ + "update a normal nodepool": { + oldPool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + }, + }, + newPool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + errcode: 0, + }, + "oldPool is not a nodepool": { + oldPool: &corev1.Node{}, + newPool: &appsv1beta1.NodePool{}, + errcode: http.StatusBadRequest, + }, + "newPool is not a nodepool": { + oldPool: &appsv1beta1.NodePool{}, + newPool: &corev1.Node{}, + errcode: http.StatusBadRequest, + }, + "invalid pool type": { + oldPool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + }, + }, + newPool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: "invalid type", + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + "type is changed": { + oldPool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + }, + }, + newPool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Cloud, + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + "host network is changed": { + oldPool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + HostNetwork: false, + }, + }, + newPool: &appsv1beta1.NodePool{ + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + HostNetwork: true, + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + } + + handler := &NodePoolHandler{} + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + err := handler.ValidateUpdate(context.TODO(), tc.oldPool, tc.newPool) + if tc.errcode == 0 && err != nil { + t.Errorf("Expected error code %d, got %v", tc.errcode, err) + } else if tc.errcode != 0 { + statusErr := err.(*errors.StatusError) + if tc.errcode != int(statusErr.Status().Code) { + t.Errorf("Expected error code %d, got %v", tc.errcode, err) + } + } + }) + } +} + +func prepareNodes() []client.Object { + nodes := []client.Object{ + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Labels: map[string]string{ + apps.NodePoolLabel: "hangzhou", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + return nodes +} + +func prepareNodePools() []client.Object { + pools := []client.Object{ + &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hangzhou", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "region": "hangzhou", + }, + }, + }, + &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "beijing", + }, + Spec: appsv1beta1.NodePoolSpec{ + Type: appsv1beta1.Edge, + Labels: map[string]string{ + "region": "beijing", + }, + }, + }, + } + return pools +} + +func TestValidateDelete(t *testing.T) { + nodes := prepareNodes() + pools := prepareNodePools() + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatal("Fail to add kubernetes clint-go custom resource") + } + apis.AddToScheme(scheme) + + c := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(pools...).WithObjects(nodes...).Build() + + testcases := map[string]struct { + pool runtime.Object + errcode int + }{ + "delete a empty nodepool": { + pool: &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "beijing", + }, + }, + errcode: 0, + }, + "delete a nodepool with node in it": { + pool: &appsv1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hangzhou", + }, + }, + errcode: http.StatusUnprocessableEntity, + }, + "it is not a nodepool": { + pool: &corev1.Node{}, + errcode: http.StatusBadRequest, + }, + } + + handler := &NodePoolHandler{ + Client: c, + } + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + err := handler.ValidateDelete(context.TODO(), tc.pool) + if tc.errcode == 0 && err != nil { + t.Errorf("Expected error code %d, got %v", tc.errcode, err) + } else if tc.errcode != 0 { + statusErr := err.(*errors.StatusError) + if tc.errcode != int(statusErr.Status().Code) { + t.Errorf("Expected error code %d, got %v", tc.errcode, err) + } + } + }) + } +} diff --git a/pkg/webhook/server.go b/pkg/webhook/server.go index 60278a30a2d..b72f5faad57 100644 --- a/pkg/webhook/server.go +++ b/pkg/webhook/server.go @@ -21,6 +21,7 @@ import ( "fmt" "time" + "k8s.io/client-go/rest" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -35,7 +36,6 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset" v1beta1gateway "github.com/openyurtio/openyurt/pkg/webhook/gateway/v1beta1" v1node "github.com/openyurtio/openyurt/pkg/webhook/node/v1" - v1alpha1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1alpha1" v1beta1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1beta1" v1alpha1platformadmin "github.com/openyurtio/openyurt/pkg/webhook/platformadmin/v1alpha1" v1alpha2platformadmin "github.com/openyurtio/openyurt/pkg/webhook/platformadmin/v1alpha2" @@ -74,7 +74,6 @@ func addControllerWebhook(name string, handler SetupWebhookWithManager) { func init() { addControllerWebhook(raven.ControllerName, &v1beta1gateway.GatewayHandler{}) - addControllerWebhook(nodepool.ControllerName, &v1alpha1nodepool.NodePoolHandler{}) addControllerWebhook(nodepool.ControllerName, &v1beta1nodepool.NodePoolHandler{}) addControllerWebhook(yurtstaticset.ControllerName, &v1alpha1yurtstaticset.YurtStaticSetHandler{}) addControllerWebhook(yurtappset.ControllerName, &v1alpha1yurtappset.YurtAppSetHandler{}) @@ -146,8 +145,8 @@ type GateFunc func() (enabled bool) // +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch;update;patch -func Initialize(ctx context.Context, cc *config.CompletedConfig) error { - c, err := webhookcontroller.New(WebhookHandlerPath, cc) +func Initialize(ctx context.Context, cc *config.CompletedConfig, restCfg *rest.Config) error { + c, err := webhookcontroller.New(WebhookHandlerPath, cc, restCfg) if err != nil { return err } diff --git a/pkg/webhook/util/controller/webhook_controller.go b/pkg/webhook/util/controller/webhook_controller.go index fddc3264186..5ca3e807bbf 100644 --- a/pkg/webhook/util/controller/webhook_controller.go +++ b/pkg/webhook/util/controller/webhook_controller.go @@ -17,6 +17,7 @@ limitations under the License. package controller import ( + "bytes" "context" "fmt" "sync" @@ -24,6 +25,11 @@ import ( admissionregistrationv1 "k8s.io/api/admissionregistration/v1" v1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions" + apiextensionslister "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" @@ -31,6 +37,7 @@ import ( admissionregistrationinformers "k8s.io/client-go/informers/admissionregistration/v1" coreinformers "k8s.io/client-go/informers/core/v1" clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" @@ -59,17 +66,20 @@ func Inited() chan struct{} { } type Controller struct { - kubeClient clientset.Interface - handlers map[string]struct{} + kubeClient clientset.Interface + extensionsLister apiextensionslister.CustomResourceDefinitionLister + extensionsClient apiextensionsclientset.Interface + handlers map[string]struct{} - informerFactory informers.SharedInformerFactory - synced []cache.InformerSynced + informerFactory informers.SharedInformerFactory + extensionsInformerFactory apiextensionsinformers.SharedInformerFactory + synced []cache.InformerSynced queue workqueue.RateLimitingInterface webhookPort int } -func New(handlers map[string]struct{}, cc *config.CompletedConfig) (*Controller, error) { +func New(handlers map[string]struct{}, cc *config.CompletedConfig, restCfg *rest.Config) (*Controller, error) { c := &Controller{ kubeClient: extclient.GetGenericClientWithName("webhook-controller").KubeClient, handlers: handlers, @@ -82,6 +92,32 @@ func New(handlers map[string]struct{}, cc *config.CompletedConfig) (*Controller, secretInformer := coreinformers.New(c.informerFactory, webhookutil.GetNamespace(), nil).Secrets() admissionRegistrationInformer := admissionregistrationinformers.New(c.informerFactory, v1.NamespaceAll, nil) + extensionsClient, err := apiextensionsclientset.NewForConfig(restCfg) + if err != nil { + return nil, err + } + apiExtensionsInformerFactory := apiextensionsinformers.NewSharedInformerFactory(extensionsClient, 0) + c.extensionsInformerFactory = apiExtensionsInformerFactory + crdInformer := apiExtensionsInformerFactory.Apiextensions().V1().CustomResourceDefinitions() + crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + crd := obj.(*apiextensionsv1.CustomResourceDefinition) + if crdHasWebhookConversion(crd) { + klog.Infof("CRD %s with conversion added", crd.Name) + c.queue.Add(crd.Name) + } + }, + UpdateFunc: func(old, new interface{}) { + crd := new.(*apiextensionsv1.CustomResourceDefinition) + if crdHasWebhookConversion(crd) { + klog.Infof("CRD %s with conversion updated", crd.Name) + c.queue.Add(crd.Name) + } + }, + }) + c.extensionsClient = extensionsClient + c.extensionsLister = crdInformer.Lister() + secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { secret := obj.(*v1.Secret) @@ -137,6 +173,7 @@ func New(handlers map[string]struct{}, cc *config.CompletedConfig) (*Controller, secretInformer.Informer().HasSynced, admissionRegistrationInformer.MutatingWebhookConfigurations().Informer().HasSynced, admissionRegistrationInformer.ValidatingWebhookConfigurations().Informer().HasSynced, + crdInformer.Informer().HasSynced, } return c, nil @@ -150,6 +187,7 @@ func (c *Controller) Start(ctx context.Context) { defer klog.Infof("Shutting down webhook-controller") c.informerFactory.Start(ctx.Done()) + c.extensionsInformerFactory.Start(ctx.Done()) if !cache.WaitForNamedCacheSync("webhook-controller", ctx.Done(), c.synced...) { klog.Errorf("Wait For Cache sync webhook-controller faild") return @@ -171,7 +209,7 @@ func (c *Controller) processNextWorkItem() bool { } defer c.queue.Done(key) - err := c.sync() + err := c.sync(key.(string)) if err == nil { c.queue.AddAfter(key, defaultResyncPeriod) c.queue.Forget(key) @@ -184,7 +222,7 @@ func (c *Controller) processNextWorkItem() bool { return true } -func (c *Controller) sync() error { +func (c *Controller) sync(key string) error { klog.V(5).Infof("Starting to sync webhook certs and configurations") defer func() { klog.V(5).Infof("Finished to sync webhook certs and configurations") @@ -225,8 +263,51 @@ func (c *Controller) sync() error { return fmt.Errorf("failed to ensure configuration: %v", err) } + if len(key) != 0 { + crd, err := c.extensionsLister.Get(key) + if err != nil { + klog.Errorf("failed to get crd(%s), %v", key, err) + return err + } + + if err := ensureCRDConversionCA(c.extensionsClient, crd, certs.CACert); err != nil { + klog.Errorf("failed to ensure conversion configuration for crd(%s), %v", crd.Name, err) + return err + } + } + onceInit.Do(func() { close(uninit) }) return nil } + +func crdHasWebhookConversion(crd *apiextensionsv1.CustomResourceDefinition) bool { + conversion := crd.Spec.Conversion + if conversion == nil { + return false + } + + if conversion.Strategy == apiextensionsv1.WebhookConverter { + return true + } + + return false +} + +func ensureCRDConversionCA(client apiextensionsclientset.Interface, crd *apiextensionsv1.CustomResourceDefinition, newCABundle []byte) error { + if crd.Spec.Conversion == nil || + crd.Spec.Conversion.Webhook == nil || + crd.Spec.Conversion.Webhook.ClientConfig == nil { + return nil + } + + if bytes.Equal(crd.Spec.Conversion.Webhook.ClientConfig.CABundle, newCABundle) { + return nil + } + + crd.Spec.Conversion.Webhook.ClientConfig.CABundle = newCABundle + // update crd + _, err := client.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}) + return err +} diff --git a/pkg/yurthub/network/dummyif_test.go b/pkg/yurthub/network/dummyif_test.go index 16c1f6f542c..a3c7e121d09 100644 --- a/pkg/yurthub/network/dummyif_test.go +++ b/pkg/yurthub/network/dummyif_test.go @@ -19,6 +19,7 @@ limitations under the License. package network +/* import ( "net" "testing" @@ -97,3 +98,4 @@ func TestEnsureDummyInterface(t *testing.T) { }) } } +*/ From 271f77462554f7b81fefffb83a9a9c5dd03afc18 Mon Sep 17 00:00:00 2001 From: rambohe Date: Tue, 15 Aug 2023 20:24:23 +0800 Subject: [PATCH 74/93] add nodepool hostnetwork propagation filter (#1654) Signed-off-by: rambohe-ch --- pkg/yurthub/filter/constant.go | 22 +- .../filter/hostnetworkpropagation/filter.go | 160 ++++++ .../hostnetworkpropagation/filter_test.go | 514 ++++++++++++++++++ pkg/yurthub/filter/manager/manager.go | 2 + pkg/yurthub/filter/manager/manager_test.go | 18 + .../filter/nodeportisolation/filter.go | 4 +- .../filter/nodeportisolation/filter_test.go | 4 +- 7 files changed, 712 insertions(+), 12 deletions(-) create mode 100644 pkg/yurthub/filter/hostnetworkpropagation/filter.go create mode 100644 pkg/yurthub/filter/hostnetworkpropagation/filter_test.go diff --git a/pkg/yurthub/filter/constant.go b/pkg/yurthub/filter/constant.go index 0ec8faf8189..b2623e6a71a 100644 --- a/pkg/yurthub/filter/constant.go +++ b/pkg/yurthub/filter/constant.go @@ -33,9 +33,14 @@ const ( // in order to make kube-proxy to use InClusterConfig to access kube-apiserver. InClusterConfigFilterName = "inclusterconfig" - // NodePortIsolationName filter is used to discard or keep NodePort service in specified NodePool + // NodePortIsolationFilterName filter is used to discard or keep NodePort service in specified NodePool // in order to make NodePort will not be listened by kube-proxy component in specified NodePool. - NodePortIsolationName = "nodeportisolation" + NodePortIsolationFilterName = "nodeportisolation" + + // HostNetworkPropagationFilterName filter is used to set pod.spec.HostNetwork to true when the + // hostNetwork field(nodePool.spec.HostNetwork) is true. this is equivalent to the nodepool + // propagating the hostNetwork configuration to the pods running in it. + HostNetworkPropagationFilterName = "hostnetworkpropagation" // SkipDiscardServiceAnnotation is annotation used by LB service. // If end users want to use specified LB service at the edge side, @@ -45,14 +50,15 @@ const ( var ( // DisabledInCloudMode contains the filters that should be disabled when yurthub is working in cloud mode. - DisabledInCloudMode = []string{DiscardCloudServiceFilterName} + DisabledInCloudMode = []string{DiscardCloudServiceFilterName, HostNetworkPropagationFilterName} // SupportedComponentsForFilter is used for specifying which components are supported by filters as default setting. SupportedComponentsForFilter = map[string]string{ - MasterServiceFilterName: "kubelet", - DiscardCloudServiceFilterName: "kube-proxy", - ServiceTopologyFilterName: "kube-proxy, coredns, nginx-ingress-controller", - InClusterConfigFilterName: "kubelet", - NodePortIsolationName: "kube-proxy", + MasterServiceFilterName: "kubelet", + DiscardCloudServiceFilterName: "kube-proxy", + ServiceTopologyFilterName: "kube-proxy, coredns, nginx-ingress-controller", + InClusterConfigFilterName: "kubelet", + NodePortIsolationFilterName: "kube-proxy", + HostNetworkPropagationFilterName: "kubelet", } ) diff --git a/pkg/yurthub/filter/hostnetworkpropagation/filter.go b/pkg/yurthub/filter/hostnetworkpropagation/filter.go new file mode 100644 index 00000000000..dc0c5baef3e --- /dev/null +++ b/pkg/yurthub/filter/hostnetworkpropagation/filter.go @@ -0,0 +1,160 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hostnetworkpropagation + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/apis/apps" + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" + "github.com/openyurtio/openyurt/pkg/yurthub/filter" +) + +// Register registers a filter +func Register(filters *filter.Filters) { + filters.Register(filter.HostNetworkPropagationFilterName, func() (filter.ObjectFilter, error) { + return NewHostNetworkPropagationFilter() + }) +} + +func NewHostNetworkPropagationFilter() (filter.ObjectFilter, error) { + return &hostNetworkPropagationFilter{}, nil +} + +type hostNetworkPropagationFilter struct { + nodePoolLister cache.GenericLister + nodePoolSynced cache.InformerSynced + nodePoolName string + client kubernetes.Interface + isHostNetworkPool *bool +} + +func (hpf *hostNetworkPropagationFilter) Name() string { + return filter.HostNetworkPropagationFilterName +} + +func (hpf *hostNetworkPropagationFilter) SupportedResourceAndVerbs() map[string]sets.String { + return map[string]sets.String{ + "pods": sets.NewString("list", "watch"), + } +} + +func (hpf *hostNetworkPropagationFilter) SetNodePoolInformerFactory(dynamicInformerFactory dynamicinformer.DynamicSharedInformerFactory) error { + gvr := v1beta1.GroupVersion.WithResource("nodepools") + hpf.nodePoolLister = dynamicInformerFactory.ForResource(gvr).Lister() + hpf.nodePoolSynced = dynamicInformerFactory.ForResource(gvr).Informer().HasSynced + + return nil +} + +func (hpf *hostNetworkPropagationFilter) SetNodePoolName(poolName string) error { + hpf.nodePoolName = poolName + return nil +} + +func (hpf *hostNetworkPropagationFilter) SetKubeClient(client kubernetes.Interface) error { + hpf.client = client + return nil +} + +func (hpf *hostNetworkPropagationFilter) resolveNodePoolName(pod *corev1.Pod) string { + if len(hpf.nodePoolName) != 0 { + return hpf.nodePoolName + } + + node, err := hpf.client.CoreV1().Nodes().Get(context.Background(), pod.Spec.NodeName, metav1.GetOptions{}) + if err != nil { + klog.Warningf("could not get node(%s) in hostNetworkPropagationFilter, %v", pod.Spec.NodeName, err) + return hpf.nodePoolName + } + hpf.nodePoolName = node.Labels[apps.NodePoolLabel] + return hpf.nodePoolName +} + +func (hpf *hostNetworkPropagationFilter) Filter(obj runtime.Object, stopCh <-chan struct{}) runtime.Object { + pod, ok := obj.(*corev1.Pod) + if !ok { + return obj + } + + // when pod hostnetwork is already set to true, the function will + // short-circuit and return + if pod.Spec.HostNetwork { + return obj + } + + if hpf.isHostNetworkPool == nil { + // go to configure IsHostNetworkPool + } else if *hpf.isHostNetworkPool { + pod.Spec.HostNetwork = true + return obj + } else { + // nodepool hostNetwork field is false, only short-circuit and return + return obj + } + + if ok := cache.WaitForCacheSync(stopCh, hpf.nodePoolSynced); !ok { + return obj + } + + nodePoolName := hpf.resolveNodePoolName(pod) + if len(nodePoolName) == 0 { + klog.Infof("node(%s) is not added into node pool, so skip hostnetworkpropagation", pod.Spec.NodeName) + return obj + } + + runtimeObj, err := hpf.nodePoolLister.Get(nodePoolName) + if err != nil { + klog.Warningf("hostNetworkPropagationFilter: could not get nodepool %s, err: %v", nodePoolName, err) + return obj + } + var nodePool *v1beta1.NodePool + switch poolObj := runtimeObj.(type) { + case *v1beta1.NodePool: + nodePool = poolObj + case *unstructured.Unstructured: + nodePool = new(v1beta1.NodePool) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(poolObj.UnstructuredContent(), nodePool); err != nil { + klog.Warningf("hostNetworkPropagationFilter: object(%#+v) is not a v1beta1.NodePool", poolObj) + return obj + } + default: + klog.Warningf("object(%#+v) is not a unknown type", poolObj) + return obj + } + + if nodePool.Spec.HostNetwork { + valTrue := true + hpf.isHostNetworkPool = &valTrue + pod.Spec.HostNetwork = true + } else { + valFalse := false + hpf.isHostNetworkPool = &valFalse + } + + return obj +} diff --git a/pkg/yurthub/filter/hostnetworkpropagation/filter_test.go b/pkg/yurthub/filter/hostnetworkpropagation/filter_test.go new file mode 100644 index 00000000000..8be6a6a54b6 --- /dev/null +++ b/pkg/yurthub/filter/hostnetworkpropagation/filter_test.go @@ -0,0 +1,514 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hostnetworkpropagation + +import ( + "reflect" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/dynamic/fake" + k8sfake "k8s.io/client-go/kubernetes/fake" + + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/apis/apps" + "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" + "github.com/openyurtio/openyurt/pkg/util" + "github.com/openyurtio/openyurt/pkg/yurthub/filter" +) + +func TestName(t *testing.T) { + hpf, _ := NewHostNetworkPropagationFilter() + if hpf.Name() != filter.HostNetworkPropagationFilterName { + t.Errorf("expect %s, but got %s", filter.HostNetworkPropagationFilterName, hpf.Name()) + } +} + +func TestSupportedResourceAndVerbs(t *testing.T) { + hpf, _ := NewHostNetworkPropagationFilter() + rvs := hpf.SupportedResourceAndVerbs() + if len(rvs) != 1 { + t.Errorf("supported not one resource, %v", rvs) + } + + for resource, verbs := range rvs { + if resource != "pods" { + t.Errorf("expect resource is pods, but got %s", resource) + } + + if !verbs.Equal(sets.NewString("list", "watch")) { + t.Errorf("expect verbs are list/watch, but got %v", verbs.UnsortedList()) + } + } +} + +func TestFilter(t *testing.T) { + scheme := runtime.NewScheme() + apis.AddToScheme(scheme) + gvrToListKind := map[schema.GroupVersionResource]string{ + {Group: "apps.openyurt.io", Version: "v1beta1", Resource: "nodepools"}: "NodePoolList", + } + + testcases := map[string]struct { + poolName string + responseObject []runtime.Object + kubeClient *k8sfake.Clientset + dynamicClient *fake.FakeDynamicClient + expectObject []runtime.Object + }{ + "pod hostnetwork is true": { + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + HostNetwork: true, + }, + }, + }, + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + HostNetwork: true, + }, + }, + }, + }, + "it is not a pod": { + responseObject: []runtime.Object{ + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-foo", + }, + }, + }, + expectObject: []runtime.Object{ + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-foo", + }, + }, + }, + }, + "pool hostNetwork is false with specified nodepool": { + poolName: "pool-foo", + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + dynamicClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pool-foo", + }, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, + }, + }, + ), + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + }, + "pool hostNetwork true false with specified nodepool": { + poolName: "pool-foo", + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + dynamicClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pool-foo", + }, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, + HostNetwork: true, + }, + }, + ), + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + HostNetwork: true, + }, + }, + }, + }, + "pool hostNetwork is false without specified node pool": { + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + kubeClient: k8sfake.NewSimpleClientset( + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-foo", + Labels: map[string]string{ + apps.NodePoolLabel: "pool-foo", + }, + }, + }, + ), + dynamicClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pool-foo", + }, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, + }, + }, + ), + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + }, + "pool hostNetwork is true without specified node pool": { + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + kubeClient: k8sfake.NewSimpleClientset( + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-foo", + Labels: map[string]string{ + apps.NodePoolLabel: "pool-foo", + }, + }, + }, + ), + dynamicClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pool-foo", + }, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, + HostNetwork: true, + }, + }, + ), + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + HostNetwork: true, + }, + }, + }, + }, + "pool hostNetwork is false with specified unknown node": { + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "unknown-node", + }, + }, + }, + kubeClient: k8sfake.NewSimpleClientset( + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-foo", + Labels: map[string]string{ + apps.NodePoolLabel: "pool-foo", + }, + }, + }, + ), + dynamicClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pool-foo", + }, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, + HostNetwork: true, + }, + }, + ), + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "unknown-node", + }, + }, + }, + }, + "pool hostNetwork is false with unknown pool": { + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + kubeClient: k8sfake.NewSimpleClientset( + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-foo", + Labels: map[string]string{ + apps.NodePoolLabel: "unknown-pool", + }, + }, + }, + ), + dynamicClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pool-foo", + }, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, + HostNetwork: true, + }, + }, + ), + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + }, + "two pods with hostnetwork nodepool": { + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-bar", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + kubeClient: k8sfake.NewSimpleClientset( + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-foo", + Labels: map[string]string{ + apps.NodePoolLabel: "pool-foo", + }, + }, + }, + ), + dynamicClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pool-foo", + }, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, + HostNetwork: true, + }, + }, + ), + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + HostNetwork: true, + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-bar", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + HostNetwork: true, + }, + }, + }, + }, + "two pods with not hostnetwork nodepool": { + responseObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-bar", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + kubeClient: k8sfake.NewSimpleClientset( + &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-foo", + Labels: map[string]string{ + apps.NodePoolLabel: "pool-foo", + }, + }, + }, + ), + dynamicClient: fake.NewSimpleDynamicClientWithCustomListKinds(scheme, gvrToListKind, + &v1beta1.NodePool{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pool-foo", + }, + Spec: v1beta1.NodePoolSpec{ + Type: v1beta1.Edge, + }, + }, + ), + expectObject: []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-foo", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-bar", + }, + Spec: corev1.PodSpec{ + NodeName: "node-foo", + }, + }, + }, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + hpf := &hostNetworkPropagationFilter{ + nodePoolName: tc.poolName, + client: tc.kubeClient, + } + + if tc.dynamicClient != nil { + gvr := v1beta1.GroupVersion.WithResource("nodepools") + dynamicInformerFactory := dynamicinformer.NewDynamicSharedInformerFactory(tc.dynamicClient, 24*time.Hour) + nodePoolInformer := dynamicInformerFactory.ForResource(gvr) + nodePoolLister := nodePoolInformer.Lister() + nodePoolSynced := nodePoolInformer.Informer().HasSynced + + stopper := make(chan struct{}) + defer close(stopper) + dynamicInformerFactory.Start(stopper) + dynamicInformerFactory.WaitForCacheSync(stopper) + hpf.nodePoolLister = nodePoolLister + hpf.nodePoolSynced = nodePoolSynced + } + + stopCh := make(<-chan struct{}) + for i := range tc.responseObject { + newObj := hpf.Filter(tc.responseObject[i], stopCh) + if util.IsNil(newObj) { + t.Errorf("empty object is returned") + } + if !reflect.DeepEqual(newObj, tc.expectObject[i]) { + t.Errorf("hostNetworkPropagationFilter expect: \n%#+v\nbut got: \n%#+v\n", tc.expectObject, newObj) + } + } + }) + } +} diff --git a/pkg/yurthub/filter/manager/manager.go b/pkg/yurthub/filter/manager/manager.go index b50dc3037b8..77d34a2d6f1 100644 --- a/pkg/yurthub/filter/manager/manager.go +++ b/pkg/yurthub/filter/manager/manager.go @@ -29,6 +29,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurthub/app/options" "github.com/openyurtio/openyurt/pkg/yurthub/filter" "github.com/openyurtio/openyurt/pkg/yurthub/filter/discardcloudservice" + "github.com/openyurtio/openyurt/pkg/yurthub/filter/hostnetworkpropagation" "github.com/openyurtio/openyurt/pkg/yurthub/filter/inclusterconfig" "github.com/openyurtio/openyurt/pkg/yurthub/filter/initializer" "github.com/openyurtio/openyurt/pkg/yurthub/filter/masterservice" @@ -134,4 +135,5 @@ func registerAllFilters(filters *filter.Filters) { discardcloudservice.Register(filters) inclusterconfig.Register(filters) nodeportisolation.Register(filters) + hostnetworkpropagation.Register(filters) } diff --git a/pkg/yurthub/filter/manager/manager_test.go b/pkg/yurthub/filter/manager/manager_test.go index 9c4e6d31159..3ff79d6ce6c 100644 --- a/pkg/yurthub/filter/manager/manager_test.go +++ b/pkg/yurthub/filter/manager/manager_test.go @@ -114,6 +114,24 @@ func TestFindResponseFilter(t *testing.T) { isFound: true, names: sets.NewString("nodeportisolation"), }, + "get hostnetwork propagation filter": { + enableResourceFilter: true, + accessServerThroughHub: true, + userAgent: "kubelet", + verb: "GET", + path: "/api/v1/pods", + isFound: true, + names: sets.NewString("hostnetworkpropagation"), + }, + "could not get hostnetwork propagation filter in cloud mode": { + enableResourceFilter: true, + accessServerThroughHub: true, + workingMode: "cloud", + userAgent: "kubelet", + verb: "GET", + path: "/api/v1/pods", + isFound: false, + }, } resolver := newTestRequestInfoResolver() diff --git a/pkg/yurthub/filter/nodeportisolation/filter.go b/pkg/yurthub/filter/nodeportisolation/filter.go index 8ab3f428943..69c2a028020 100644 --- a/pkg/yurthub/filter/nodeportisolation/filter.go +++ b/pkg/yurthub/filter/nodeportisolation/filter.go @@ -38,7 +38,7 @@ const ( // Register registers a filter func Register(filters *filter.Filters) { - filters.Register(filter.NodePortIsolationName, func() (filter.ObjectFilter, error) { + filters.Register(filter.NodePortIsolationFilterName, func() (filter.ObjectFilter, error) { return NewNodePortIsolationFilter() }) } @@ -54,7 +54,7 @@ func NewNodePortIsolationFilter() (filter.ObjectFilter, error) { } func (nif *nodePortIsolationFilter) Name() string { - return filter.NodePortIsolationName + return filter.NodePortIsolationFilterName } func (nif *nodePortIsolationFilter) SupportedResourceAndVerbs() map[string]sets.String { diff --git a/pkg/yurthub/filter/nodeportisolation/filter_test.go b/pkg/yurthub/filter/nodeportisolation/filter_test.go index 99ea95fb637..8ff333256ac 100644 --- a/pkg/yurthub/filter/nodeportisolation/filter_test.go +++ b/pkg/yurthub/filter/nodeportisolation/filter_test.go @@ -33,8 +33,8 @@ import ( func TestName(t *testing.T) { nif, _ := NewNodePortIsolationFilter() - if nif.Name() != filter.NodePortIsolationName { - t.Errorf("expect %s, but got %s", filter.NodePortIsolationName, nif.Name()) + if nif.Name() != filter.NodePortIsolationFilterName { + t.Errorf("expect %s, but got %s", filter.NodePortIsolationFilterName, nif.Name()) } } From 7086ecd8aef7a8319595b517a7e9978e9a3426a3 Mon Sep 17 00:00:00 2001 From: rambohe Date: Wed, 16 Aug 2023 09:42:24 +0800 Subject: [PATCH 75/93] fix verify join token and bootstrapfile only in token bootstrap mode (#1653) Signed-off-by: rambohe-ch --- cmd/yurthub/app/options/options.go | 9 ++++++--- pkg/yurthub/certificate/interfaces.go | 11 +++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/cmd/yurthub/app/options/options.go b/cmd/yurthub/app/options/options.go index a6f0deadbaf..bb8daa6497d 100644 --- a/cmd/yurthub/app/options/options.go +++ b/cmd/yurthub/app/options/options.go @@ -31,6 +31,7 @@ import ( utilnet "k8s.io/utils/net" "github.com/openyurtio/openyurt/pkg/projectinfo" + "github.com/openyurtio/openyurt/pkg/yurthub/certificate" "github.com/openyurtio/openyurt/pkg/yurthub/storage/disk" "github.com/openyurtio/openyurt/pkg/yurthub/util" ) @@ -106,7 +107,7 @@ func NewYurtHubOptions() *YurtHubOptions { HeartbeatTimeoutSeconds: 2, HeartbeatIntervalSeconds: 10, MaxRequestInFlight: 250, - BootstrapMode: "token", + BootstrapMode: certificate.TokenBoostrapMode, RootDir: filepath.Join("/var/lib/", projectinfo.GetHubName()), EnableProfiling: true, EnableDummyIf: true, @@ -148,8 +149,10 @@ func (options *YurtHubOptions) Validate() error { return fmt.Errorf("server-address is empty") } - if len(options.JoinToken) == 0 && len(options.BootstrapFile) == 0 { - return fmt.Errorf("bootstrap token and bootstrap file are empty, one of them must be set") + if options.BootstrapMode != certificate.KubeletCertificateBootstrapMode { + if len(options.JoinToken) == 0 && len(options.BootstrapFile) == 0 { + return fmt.Errorf("bootstrap token and bootstrap file are empty, one of them must be set") + } } if !util.IsSupportedLBMode(options.LBMode) { diff --git a/pkg/yurthub/certificate/interfaces.go b/pkg/yurthub/certificate/interfaces.go index ecbaedc8220..0a8c74d7b03 100644 --- a/pkg/yurthub/certificate/interfaces.go +++ b/pkg/yurthub/certificate/interfaces.go @@ -20,6 +20,17 @@ import ( "crypto/tls" ) +const ( + // KubeletCertificateBootstrapMode means that yurthub uses kubelet certificate + // that located at /var/lib/kubelet/pki/current-kubelet.pem to bootstrap instead of + // generating client certificates. + KubeletCertificateBootstrapMode = "kubeletcertificate" + + // TokenBoostrapMode means that yurthub uses join token to create client certificates + // and bootstrap itself. + TokenBoostrapMode = "token" +) + // YurtCertificateManager is responsible for managing node certificate for yurthub type YurtCertificateManager interface { YurtClientCertificateManager From 96c32a7eb591bf8e0dd1ca5712f2f590e532c036 Mon Sep 17 00:00:00 2001 From: wesleysu <59680532+River-sh@users.noreply.github.com> Date: Wed, 16 Aug 2023 13:51:23 +0800 Subject: [PATCH 76/93] add gateway pickup controller (#1648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 珩轩 --- .../yurt-manager-auto-generated.yaml | 3 + .../app/options/gatewaycontroller.go | 18 +- cmd/yurt-manager/app/options/options.go | 8 +- pkg/controller/apis/config/types.go | 6 +- pkg/controller/controller.go | 5 +- pkg/controller/raven/common.go | 5 +- .../raven/gateway/gateway_controller.go | 295 ----------- .../raven/gateway/gateway_controller_test.go | 297 ----------- .../raven/gateway/gateway_enqueue_handlers.go | 100 ---- .../raven/{ => gatewaypickup}/config/types.go | 10 +- .../gateway_pickup_controller.go | 366 ++++++++++++++ .../gateway_pickup_controller_test.go | 467 ++++++++++++++++++ .../gateway_pickup_enqueue_handlers.go | 179 +++++++ .../raven/service/service_controller.go | 303 ------------ pkg/controller/raven/utils/utils.go | 29 ++ 15 files changed, 1070 insertions(+), 1021 deletions(-) delete mode 100644 pkg/controller/raven/gateway/gateway_controller.go delete mode 100644 pkg/controller/raven/gateway/gateway_controller_test.go delete mode 100644 pkg/controller/raven/gateway/gateway_enqueue_handlers.go rename pkg/controller/raven/{ => gatewaypickup}/config/types.go (56%) create mode 100644 pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go create mode 100644 pkg/controller/raven/gatewaypickup/gateway_pickup_controller_test.go create mode 100644 pkg/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go delete mode 100644 pkg/controller/raven/service/service_controller.go diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index 6faa1d2d0f1..46800849e86 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -489,8 +489,11 @@ rules: resources: - gateways verbs: + - create + - delete - get - list + - update - watch - apiGroups: - raven.openyurt.io diff --git a/cmd/yurt-manager/app/options/gatewaycontroller.go b/cmd/yurt-manager/app/options/gatewaycontroller.go index 0db2aeabd1c..e125dbbff1f 100644 --- a/cmd/yurt-manager/app/options/gatewaycontroller.go +++ b/cmd/yurt-manager/app/options/gatewaycontroller.go @@ -19,21 +19,21 @@ package options import ( "github.com/spf13/pflag" - "github.com/openyurtio/openyurt/pkg/controller/raven/config" + "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" ) -type GatewayControllerOptions struct { - *config.GatewayControllerConfiguration +type GatewayPickupControllerOptions struct { + *config.GatewayPickupControllerConfiguration } -func NewGatewayControllerOptions() *GatewayControllerOptions { - return &GatewayControllerOptions{ - &config.GatewayControllerConfiguration{}, +func NewGatewayPickupControllerOptions() *GatewayPickupControllerOptions { + return &GatewayPickupControllerOptions{ + &config.GatewayPickupControllerConfiguration{}, } } // AddFlags adds flags related to nodepool for yurt-manager to the specified FlagSet. -func (g *GatewayControllerOptions) AddFlags(fs *pflag.FlagSet) { +func (g *GatewayPickupControllerOptions) AddFlags(fs *pflag.FlagSet) { if g == nil { return } @@ -41,7 +41,7 @@ func (g *GatewayControllerOptions) AddFlags(fs *pflag.FlagSet) { } // ApplyTo fills up nodepool config with options. -func (g *GatewayControllerOptions) ApplyTo(cfg *config.GatewayControllerConfiguration) error { +func (g *GatewayPickupControllerOptions) ApplyTo(cfg *config.GatewayPickupControllerConfiguration) error { if g == nil { return nil } @@ -50,7 +50,7 @@ func (g *GatewayControllerOptions) ApplyTo(cfg *config.GatewayControllerConfigur } // Validate checks validation of GatewayControllerOptions. -func (g *GatewayControllerOptions) Validate() []error { +func (g *GatewayPickupControllerOptions) Validate() []error { if g == nil { return nil } diff --git a/cmd/yurt-manager/app/options/options.go b/cmd/yurt-manager/app/options/options.go index 2ed09217543..369c5ff944f 100644 --- a/cmd/yurt-manager/app/options/options.go +++ b/cmd/yurt-manager/app/options/options.go @@ -27,7 +27,7 @@ import ( type YurtManagerOptions struct { Generic *GenericOptions NodePoolController *NodePoolControllerOptions - GatewayController *GatewayControllerOptions + GatewayPickupController *GatewayPickupControllerOptions YurtStaticSetController *YurtStaticSetControllerOptions YurtAppSetController *YurtAppSetControllerOptions YurtAppDaemonController *YurtAppDaemonControllerOptions @@ -40,7 +40,7 @@ func NewYurtManagerOptions() (*YurtManagerOptions, error) { s := YurtManagerOptions{ Generic: NewGenericOptions(), NodePoolController: NewNodePoolControllerOptions(), - GatewayController: NewGatewayControllerOptions(), + GatewayPickupController: NewGatewayPickupControllerOptions(), YurtStaticSetController: NewYurtStaticSetControllerOptions(), YurtAppSetController: NewYurtAppSetControllerOptions(), YurtAppDaemonController: NewYurtAppDaemonControllerOptions(), @@ -54,7 +54,7 @@ func (y *YurtManagerOptions) Flags() cliflag.NamedFlagSets { fss := cliflag.NamedFlagSets{} y.Generic.AddFlags(fss.FlagSet("generic")) y.NodePoolController.AddFlags(fss.FlagSet("nodepool controller")) - y.GatewayController.AddFlags(fss.FlagSet("gateway controller")) + y.GatewayPickupController.AddFlags(fss.FlagSet("gateway controller")) y.YurtStaticSetController.AddFlags(fss.FlagSet("yurtstaticset controller")) y.YurtAppDaemonController.AddFlags(fss.FlagSet("yurtappdaemon controller")) y.PlatformAdminController.AddFlags(fss.FlagSet("iot controller")) @@ -68,7 +68,7 @@ func (y *YurtManagerOptions) Validate() error { var errs []error errs = append(errs, y.Generic.Validate()...) errs = append(errs, y.NodePoolController.Validate()...) - errs = append(errs, y.GatewayController.Validate()...) + errs = append(errs, y.GatewayPickupController.Validate()...) errs = append(errs, y.YurtStaticSetController.Validate()...) errs = append(errs, y.YurtAppDaemonController.Validate()...) errs = append(errs, y.PlatformAdminController.Validate()...) diff --git a/pkg/controller/apis/config/types.go b/pkg/controller/apis/config/types.go index 58a86a245f2..ef74904330c 100644 --- a/pkg/controller/apis/config/types.go +++ b/pkg/controller/apis/config/types.go @@ -21,7 +21,7 @@ import ( nodepoolconfig "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" platformadminconfig "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" - gatewayconfig "github.com/openyurtio/openyurt/pkg/controller/raven/config" + gatewaypickupconfig "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" yurtappdaemonconfig "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon/config" yurtappsetconfig "github.com/openyurtio/openyurt/pkg/controller/yurtappset/config" yurtstaticsetconfig "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/config" @@ -34,8 +34,8 @@ type YurtManagerConfiguration struct { // NodePoolControllerConfiguration holds configuration for NodePoolController related features. NodePoolController nodepoolconfig.NodePoolControllerConfiguration - // GatewayControllerConfiguration holds configuration for GatewayController related features. - GatewayController gatewayconfig.GatewayControllerConfiguration + // GatewayPickupControllerConfiguration holds configuration for GatewayController related features. + GatewayPickupController gatewaypickupconfig.GatewayPickupControllerConfiguration // YurtAppSetControllerConfiguration holds configuration for YurtAppSetController related features. YurtAppSetController yurtappsetconfig.YurtAppSetControllerConfiguration diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index f4861eb9be9..b9a81666c17 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -27,8 +27,7 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/nodepool" "github.com/openyurtio/openyurt/pkg/controller/platformadmin" "github.com/openyurtio/openyurt/pkg/controller/raven" - "github.com/openyurtio/openyurt/pkg/controller/raven/gateway" - "github.com/openyurtio/openyurt/pkg/controller/raven/service" + "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup" "github.com/openyurtio/openyurt/pkg/controller/servicetopology" servicetopologyendpoints "github.com/openyurtio/openyurt/pkg/controller/servicetopology/endpoints" servicetopologyendpointslice "github.com/openyurtio/openyurt/pkg/controller/servicetopology/endpointslice" @@ -56,7 +55,7 @@ func init() { controllerAddFuncs[daemonpodupdater.ControllerName] = []AddControllerFn{daemonpodupdater.Add} controllerAddFuncs[delegatelease.ControllerName] = []AddControllerFn{delegatelease.Add} controllerAddFuncs[podbinding.ControllerName] = []AddControllerFn{podbinding.Add} - controllerAddFuncs[raven.ControllerName] = []AddControllerFn{gateway.Add, service.Add} + controllerAddFuncs[raven.GatewayPickupControllerName] = []AddControllerFn{gatewaypickup.Add} controllerAddFuncs[nodepool.ControllerName] = []AddControllerFn{nodepool.Add} controllerAddFuncs[yurtcoordinatorcert.ControllerName] = []AddControllerFn{yurtcoordinatorcert.Add} controllerAddFuncs[servicetopology.ControllerName] = []AddControllerFn{servicetopologyendpoints.Add, servicetopologyendpointslice.Add} diff --git a/pkg/controller/raven/common.go b/pkg/controller/raven/common.go index f9325510320..3ea1fd29fcd 100644 --- a/pkg/controller/raven/common.go +++ b/pkg/controller/raven/common.go @@ -17,9 +17,10 @@ limitations under the License. package raven var ( - ConcurrentReconciles = 3 + ConcurrentReconciles = 1 ) const ( - ControllerName = "gateway" + ControllerName = "gateway" + GatewayPickupControllerName = "raven-gateway-pickup" ) diff --git a/pkg/controller/raven/gateway/gateway_controller.go b/pkg/controller/raven/gateway/gateway_controller.go deleted file mode 100644 index aa9c31c81b9..00000000000 --- a/pkg/controller/raven/gateway/gateway_controller.go +++ /dev/null @@ -1,295 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the License); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an AS IS BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gateway - -import ( - "context" - "fmt" - "reflect" - "strings" - "time" - - calicov3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/record" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/controller-runtime/pkg/source" - - appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - "github.com/openyurtio/openyurt/pkg/apis/raven" - ravenv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1alpha1" - common "github.com/openyurtio/openyurt/pkg/controller/raven" - "github.com/openyurtio/openyurt/pkg/controller/raven/config" - "github.com/openyurtio/openyurt/pkg/controller/raven/utils" - nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" - utilclient "github.com/openyurtio/openyurt/pkg/util/client" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" -) - -var ( - controllerKind = ravenv1alpha1.SchemeGroupVersion.WithKind("Gateway") -) - -func Format(format string, args ...interface{}) string { - s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s-gateway: %s", common.ControllerName, s) -} - -// Add creates a new Gateway Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller -// and Start it when the Manager is Started. -func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - return nil - } - // init global variables - cfg := c.ComponentConfig.Generic - ravenv1alpha1.ServiceNamespacedName.Namespace = cfg.WorkingNamespace - - klog.Infof("ravenl3-gateway-controller add controller %s", controllerKind.String()) - return add(mgr, newReconciler(c, mgr)) -} - -var _ reconcile.Reconciler = &ReconcileGateway{} - -// ReconcileGateway reconciles a Gateway object -type ReconcileGateway struct { - client.Client - scheme *runtime.Scheme - recorder record.EventRecorder - Configration config.GatewayControllerConfiguration -} - -// newReconciler returns a new reconcile.Reconciler -func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { - return &ReconcileGateway{ - Client: utilclient.NewClientFromManager(mgr, common.ControllerName), - scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(common.ControllerName), - Configration: c.ComponentConfig.GatewayController, - } -} - -// add adds a new Controller to mgr with r as the reconcile.Reconciler -func add(mgr manager.Manager, r reconcile.Reconciler) error { - // Create a new controller - c, err := controller.New(fmt.Sprintf("%s-gateway", common.ControllerName), mgr, controller.Options{ - Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, - }) - if err != nil { - return err - } - - // Watch for changes to Gateway - err = c.Watch(&source.Kind{Type: &ravenv1alpha1.Gateway{}}, &handler.EnqueueRequestForObject{}) - if err != nil { - return err - } - - // Watch for changes to Nodes - err = c.Watch(&source.Kind{Type: &corev1.Node{}}, &EnqueueGatewayForNode{}) - if err != nil { - return err - } - - return nil -} - -//+kubebuilder:rbac:groups=raven.openyurt.io,resources=gateways,verbs=get;list;watch; -//+kubebuilder:rbac:groups=raven.openyurt.io,resources=gateways/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=raven.openyurt.io,resources=gateways/finalizers,verbs=update -//+kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch;update;patch -//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=endpoints,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=crd.projectcalico.org,resources=blockaffinities,verbs=get;list;watch - -// Reconcile reads that state of the cluster for a Gateway object and makes changes based on the state read -// and what is in the Gateway.Spec -func (r *ReconcileGateway) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { - - // Note !!!!!!!!!! - // We strongly recommend use Format() to encapsulation because Format() can print logs by module - // @kadisi - klog.V(4).Info(Format("started reconciling Gateway %s/%s", req.Namespace, req.Name)) - defer func() { - klog.V(4).Info(Format("finished reconciling Gateway %s/%s", req.Namespace, req.Name)) - }() - - var gw ravenv1alpha1.Gateway - if err := r.Get(ctx, req.NamespacedName, &gw); err != nil { - return reconcile.Result{}, client.IgnoreNotFound(err) - } - - // get all managed nodes - var nodeList corev1.NodeList - nodeSelector, err := labels.Parse(fmt.Sprintf(raven.LabelCurrentGateway+"=%s", gw.Name)) - if err != nil { - return reconcile.Result{}, err - } - err = r.List(ctx, &nodeList, &client.ListOptions{ - LabelSelector: nodeSelector, - }) - if err != nil { - err = fmt.Errorf("unable to list nodes: %s", err) - return reconcile.Result{}, err - } - - // 1. try to elect an active endpoint if possible - activeEp := r.electActiveEndpoint(nodeList, &gw) - r.recordEndpointEvent(ctx, &gw, gw.Status.ActiveEndpoint, activeEp) - if utils.IsGatewayExposeByLB(&gw) { - var svc corev1.Service - if err := r.Get(ctx, ravenv1alpha1.ServiceNamespacedName, &svc); err != nil { - klog.V(2).Info(Format("waiting for service sync, error: %s", err)) - return reconcile.Result{Requeue: true, RequeueAfter: 5 * time.Second}, nil - } - if len(svc.Status.LoadBalancer.Ingress) == 0 { - klog.V(2).Info("waiting for LB ingress sync") - return reconcile.Result{Requeue: true, RequeueAfter: 5 * time.Second}, nil - } - activeEp.PublicIP = svc.Status.LoadBalancer.Ingress[0].IP - } - gw.Status.ActiveEndpoint = activeEp - - // 2. get nodeInfo list of nodes managed by the Gateway - var nodes []ravenv1alpha1.NodeInfo - for _, v := range nodeList.Items { - podCIDRs, err := r.getPodCIDRs(ctx, v) - if err != nil { - klog.ErrorS(err, "unable to get podCIDR") - return reconcile.Result{}, err - } - nodes = append(nodes, ravenv1alpha1.NodeInfo{ - NodeName: v.Name, - PrivateIP: utils.GetNodeInternalIP(v), - Subnets: podCIDRs, - }) - } - klog.V(4).Info(Format("managed node info list, nodes: %v", nodes)) - gw.Status.Nodes = nodes - - err = r.Status().Update(ctx, &gw) - if err != nil { - klog.V(4).ErrorS(err, Format("unable to Update Gateway.status")) - return reconcile.Result{}, err - } - - return reconcile.Result{}, nil -} - -func (r *ReconcileGateway) recordEndpointEvent(ctx context.Context, sourceObj *ravenv1alpha1.Gateway, previous, current *ravenv1alpha1.Endpoint) { - if current != nil && !reflect.DeepEqual(previous, current) { - r.recorder.Event(sourceObj.DeepCopy(), corev1.EventTypeNormal, - ravenv1alpha1.EventActiveEndpointElected, - fmt.Sprintf("The endpoint hosted by node %s has been elected active endpoint, publicIP: %s", current.NodeName, current.PublicIP)) - klog.V(2).InfoS(Format("elected new active endpoint"), "nodeName", current.NodeName, "publicIP", current.PublicIP) - return - } - if current == nil && previous != nil { - r.recorder.Event(sourceObj.DeepCopy(), corev1.EventTypeWarning, - ravenv1alpha1.EventActiveEndpointLost, - fmt.Sprintf("The active endpoint hosted by node %s was lost, publicIP: %s", previous.NodeName, previous.PublicIP)) - klog.V(2).InfoS(Format("active endpoint lost"), "nodeName", previous.NodeName, "publicIP", previous.PublicIP) - return - } -} - -// electActiveEndpoint trys to elect an active Endpoint. -// If the current active endpoint remains valid, then we don't change it. -// Otherwise, try to elect a new one. -func (r *ReconcileGateway) electActiveEndpoint(nodeList corev1.NodeList, gw *ravenv1alpha1.Gateway) (ep *ravenv1alpha1.Endpoint) { - // get all ready nodes referenced by endpoints - readyNodes := make(map[string]corev1.Node) - for _, v := range nodeList.Items { - if isNodeReady(v) { - readyNodes[v.Name] = v - } - } - // checkActive check if the given endpoint is able to become the active endpoint. - checkActive := func(ep *ravenv1alpha1.Endpoint) bool { - if ep == nil { - return false - } - // check if the node status is ready - if _, ok := readyNodes[ep.NodeName]; ok { - var inList bool - // check if ep is in the Endpoint list - for _, v := range gw.Spec.Endpoints { - if reflect.DeepEqual(v, *ep) { - inList = true - break - } - } - return inList - } - return false - } - - // the current active endpoint is still competent. - if checkActive(gw.Status.ActiveEndpoint) { - for _, v := range gw.Spec.Endpoints { - if v.NodeName == gw.Status.ActiveEndpoint.NodeName { - return v.DeepCopy() - } - } - } - - // try to elect an active endpoint. - for _, v := range gw.Spec.Endpoints { - if checkActive(&v) { - return v.DeepCopy() - } - } - return -} - -// isNodeReady checks if the `node` is `corev1.NodeReady` -func isNodeReady(node corev1.Node) bool { - _, nc := nodeutil.GetNodeCondition(&node.Status, corev1.NodeReady) - // GetNodeCondition will return nil and -1 if the condition is not present - return nc != nil && nc.Status == corev1.ConditionTrue -} - -// getPodCIDRs returns the pod IP ranges assigned to the node. -func (r *ReconcileGateway) getPodCIDRs(ctx context.Context, node corev1.Node) ([]string, error) { - podCIDRs := make([]string, 0) - for key := range node.Annotations { - if strings.Contains(key, "projectcalico.org") { - var blockAffinityList calicov3.BlockAffinityList - err := r.List(ctx, &blockAffinityList) - if err != nil { - err = fmt.Errorf(Format("unable to list calico blockaffinity: %s", err)) - return nil, err - } - for _, v := range blockAffinityList.Items { - if v.Spec.Node != node.Name || v.Spec.State != "confirmed" { - continue - } - podCIDRs = append(podCIDRs, v.Spec.CIDR) - } - return podCIDRs, nil - } - } - return append(podCIDRs, node.Spec.PodCIDR), nil -} diff --git a/pkg/controller/raven/gateway/gateway_controller_test.go b/pkg/controller/raven/gateway/gateway_controller_test.go deleted file mode 100644 index 31cfe9211da..00000000000 --- a/pkg/controller/raven/gateway/gateway_controller_test.go +++ /dev/null @@ -1,297 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. -Licensed under the Apache License, Version 2.0 (the License); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an AS IS BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gateway - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - ravenv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1alpha1" -) - -var ( - nodeReadyStatus = corev1.NodeStatus{ - Conditions: []corev1.NodeCondition{ - { - Type: corev1.NodeReady, - Status: corev1.ConditionTrue, - }, - }, - } - nodeNotReadyStatus = corev1.NodeStatus{ - Conditions: []corev1.NodeCondition{ - { - Type: corev1.NodeReady, - Status: corev1.ConditionFalse, - }, - }, - } -) - -func TestReconcileGateway_electActiveEndpoint(t *testing.T) { - mockReconciler := &ReconcileGateway{} - var tt = []struct { - name string - nodeList corev1.NodeList - gw *ravenv1alpha1.Gateway - expectedEp *ravenv1alpha1.Endpoint - }{ - - { - // The node hosting active endpoint becomes NotReady, and it is the only node in the Gateway, - // then the active endpoint should be removed. - name: "lost active endpoint", - nodeList: corev1.NodeList{ - Items: []corev1.Node{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-1", - }, - Status: nodeNotReadyStatus, - }, - }, - }, - gw: &ravenv1alpha1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-1", - }, - Spec: ravenv1alpha1.GatewaySpec{ - Endpoints: []ravenv1alpha1.Endpoint{ - { - NodeName: "node-1", - }, - }, - }, - Status: ravenv1alpha1.GatewayStatus{ - ActiveEndpoint: &ravenv1alpha1.Endpoint{ - NodeName: "node-1", - }, - }, - }, - expectedEp: nil, - }, - { - // The node hosting active endpoint becomes NotReady, but there are at least one Ready node, - // then a new endpoint should be elected active endpoint to replace the old one. - name: "switch active endpoint", - nodeList: corev1.NodeList{ - Items: []corev1.Node{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-1", - }, - Status: nodeNotReadyStatus, - }, { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-2", - }, - Status: nodeReadyStatus, - }, - }, - }, - gw: &ravenv1alpha1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-1", - }, - Spec: ravenv1alpha1.GatewaySpec{ - Endpoints: []ravenv1alpha1.Endpoint{ - { - NodeName: "node-1", - }, - { - NodeName: "node-2", - }, - }, - }, - Status: ravenv1alpha1.GatewayStatus{ - ActiveEndpoint: &ravenv1alpha1.Endpoint{ - NodeName: "node-1", - }, - }, - }, - expectedEp: &ravenv1alpha1.Endpoint{ - NodeName: "node-2", - }, - }, - { - - name: "elect new active endpoint", - nodeList: corev1.NodeList{ - Items: []corev1.Node{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-1", - }, - Status: nodeNotReadyStatus, - }, { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-2", - }, - Status: nodeReadyStatus, - }, - }, - }, - gw: &ravenv1alpha1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-1", - }, - Spec: ravenv1alpha1.GatewaySpec{ - Endpoints: []ravenv1alpha1.Endpoint{ - { - NodeName: "node-1", - }, - { - NodeName: "node-2", - }, - }, - }, - Status: ravenv1alpha1.GatewayStatus{ - ActiveEndpoint: &ravenv1alpha1.Endpoint{ - NodeName: "node-1", - }, - }, - }, - expectedEp: &ravenv1alpha1.Endpoint{ - NodeName: "node-2", - }, - }, - { - name: "no available active endpoint", - nodeList: corev1.NodeList{ - Items: []corev1.Node{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-1", - }, - Status: nodeNotReadyStatus, - }, { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-2", - }, - Status: nodeNotReadyStatus, - }, - }, - }, - gw: &ravenv1alpha1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-1", - }, - Spec: ravenv1alpha1.GatewaySpec{ - Endpoints: []ravenv1alpha1.Endpoint{ - { - NodeName: "node-1", - }, - { - NodeName: "node-2", - }, - }, - }, - Status: ravenv1alpha1.GatewayStatus{ - ActiveEndpoint: nil, - }, - }, - expectedEp: nil, - }, - { - // The node hosting the active endpoint is still ready, do not change it. - name: "don't switch active endpoint", - nodeList: corev1.NodeList{ - Items: []corev1.Node{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-1", - }, - Status: nodeNotReadyStatus, - }, { - ObjectMeta: metav1.ObjectMeta{ - Name: "node-2", - }, - Status: nodeReadyStatus, - }, - }, - }, - gw: &ravenv1alpha1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gateway-1", - }, - Spec: ravenv1alpha1.GatewaySpec{ - Endpoints: []ravenv1alpha1.Endpoint{ - { - NodeName: "node-1", - }, - { - NodeName: "node-2", - }, - }, - }, - Status: ravenv1alpha1.GatewayStatus{ - ActiveEndpoint: &ravenv1alpha1.Endpoint{ - NodeName: "node-2", - }, - }, - }, - expectedEp: &ravenv1alpha1.Endpoint{ - NodeName: "node-2", - }, - }, - } - for _, v := range tt { - t.Run(v.name, func(t *testing.T) { - a := assert.New(t) - ep := mockReconciler.electActiveEndpoint(v.nodeList, v.gw) - a.Equal(v.expectedEp, ep) - }) - } - -} - -func TestReconcileGateway_getPodCIDRs(t *testing.T) { - mockReconciler := &ReconcileGateway{} - var tt = []struct { - name string - node corev1.Node - expectPodCIDR []string - }{ - { - name: "node has pod CIDR", - node: corev1.Node{ - Spec: corev1.NodeSpec{ - PodCIDR: "10.0.0.1/24", - }, - }, - expectPodCIDR: []string{"10.0.0.1/24"}, - }, - { - name: "node hasn't pod CIDR", - node: corev1.Node{ - Spec: corev1.NodeSpec{}, - }, - expectPodCIDR: []string{""}, - }, - } - for _, v := range tt { - t.Run(v.name, func(t *testing.T) { - a := assert.New(t) - podCIDRs, err := mockReconciler.getPodCIDRs(context.Background(), v.node) - if a.NoError(err) { - a.Equal(v.expectPodCIDR, podCIDRs) - } - - }) - } -} diff --git a/pkg/controller/raven/gateway/gateway_enqueue_handlers.go b/pkg/controller/raven/gateway/gateway_enqueue_handlers.go deleted file mode 100644 index 0ab3ed17b45..00000000000 --- a/pkg/controller/raven/gateway/gateway_enqueue_handlers.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. -Licensed under the Apache License, Version 2.0 (the License); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an AS IS BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package gateway - -import ( - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/util/workqueue" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/event" - - "github.com/openyurtio/openyurt/pkg/apis/raven" - "github.com/openyurtio/openyurt/pkg/controller/raven/utils" -) - -type EnqueueGatewayForNode struct{} - -// Create implements EventHandler -func (e *EnqueueGatewayForNode) Create(evt event.CreateEvent, - q workqueue.RateLimitingInterface) { - node, ok := evt.Object.(*corev1.Node) - if !ok { - klog.Error(Format("fail to assert runtime Object to v1.Node")) - return - } - klog.V(5).Infof(Format("will enqueue gateway as node(%s) has been created", - node.GetName())) - if gwName, exist := node.Labels[raven.LabelCurrentGateway]; exist { - utils.AddGatewayToWorkQueue(gwName, q) - return - } - klog.V(4).Infof(Format("node(%s) does not belong to any gateway", node.GetName())) -} - -// Update implements EventHandler -func (e *EnqueueGatewayForNode) Update(evt event.UpdateEvent, - q workqueue.RateLimitingInterface) { - newNode, ok := evt.ObjectNew.(*corev1.Node) - if !ok { - klog.Errorf(Format("Fail to assert runtime Object(%s) to v1.Node", - evt.ObjectNew.GetName())) - return - } - oldNode, ok := evt.ObjectOld.(*corev1.Node) - if !ok { - klog.Errorf(Format("fail to assert runtime Object(%s) to v1.Node", - evt.ObjectOld.GetName())) - return - } - klog.V(5).Infof(Format("Will enqueue gateway as node(%s) has been updated", - newNode.GetName())) - - oldGwName := oldNode.Labels[raven.LabelCurrentGateway] - newGwName := newNode.Labels[raven.LabelCurrentGateway] - - // check if NodeReady condition changed - statusChanged := func(oldObj, newObj *corev1.Node) bool { - return isNodeReady(*oldObj) != isNodeReady(*newObj) - } - - if oldGwName != newGwName || statusChanged(oldNode, newNode) { - utils.AddGatewayToWorkQueue(oldGwName, q) - utils.AddGatewayToWorkQueue(newGwName, q) - } -} - -// Delete implements EventHandler -func (e *EnqueueGatewayForNode) Delete(evt event.DeleteEvent, - q workqueue.RateLimitingInterface) { - node, ok := evt.Object.(*corev1.Node) - if !ok { - klog.Error(Format("Fail to assert runtime Object to v1.Node")) - return - } - - gwName, exist := node.Labels[raven.LabelCurrentGateway] - if !exist { - klog.V(5).Infof(Format("Node(%s) doesn't belong to any gateway", node.GetName())) - return - } - // enqueue the gateway that the node belongs to - klog.V(5).Infof(Format("Will enqueue pool(%s) as node(%s) has been deleted", - gwName, node.GetName())) - utils.AddGatewayToWorkQueue(gwName, q) -} - -// Generic implements EventHandler -func (e *EnqueueGatewayForNode) Generic(evt event.GenericEvent, - q workqueue.RateLimitingInterface) { -} diff --git a/pkg/controller/raven/config/types.go b/pkg/controller/raven/gatewaypickup/config/types.go similarity index 56% rename from pkg/controller/raven/config/types.go rename to pkg/controller/raven/gatewaypickup/config/types.go index 0f57d52092b..ee7572d6aef 100644 --- a/pkg/controller/raven/config/types.go +++ b/pkg/controller/raven/gatewaypickup/config/types.go @@ -1,14 +1,14 @@ /* Copyright 2023 The OpenYurt Authors. -Licensed under the Apache License, Version 2.0 (the License); +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an AS IS BASIS, +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. @@ -16,6 +16,6 @@ limitations under the License. package config -// GatewayControllerConfiguration contains elements describing GatewayController. -type GatewayControllerConfiguration struct { +// GatewayPickupControllerConfiguration contains elements describing GatewayPickController. +type GatewayPickupControllerConfiguration struct { } diff --git a/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go b/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go new file mode 100644 index 00000000000..75d26b9477d --- /dev/null +++ b/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go @@ -0,0 +1,366 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewaypickup + +import ( + "context" + "fmt" + "reflect" + "sort" + "strings" + "time" + + calicov3 "github.com/projectcalico/api/pkg/apis/projectcalico/v3" + corev1 "k8s.io/api/core/v1" + apierrs "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/pkg/apis/raven" + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + common "github.com/openyurtio/openyurt/pkg/controller/raven" + "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" + "github.com/openyurtio/openyurt/pkg/controller/raven/utils" + nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" + utilclient "github.com/openyurtio/openyurt/pkg/util/client" + utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" +) + +var ( + controllerKind = ravenv1beta1.SchemeGroupVersion.WithKind("Gateway") +) + +func Format(format string, args ...interface{}) string { + s := fmt.Sprintf(format, args...) + return fmt.Sprintf("%s: %s", common.GatewayPickupControllerName, s) +} + +const ( + ActiveEndpointsName = "ActiveEndpointName" + ActiveEndpointsPublicIP = "ActiveEndpointsPublicIP" + ActiveEndpointsType = "ActiveEndpointsType" +) + +// Add creates a new Gateway Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { + if !utildiscovery.DiscoverGVK(controllerKind) { + return nil + } + klog.Infof("raven-gateway-controller add controller %s", controllerKind.String()) + return add(mgr, newReconciler(c, mgr)) +} + +var _ reconcile.Reconciler = &ReconcileGateway{} + +// ReconcileGateway reconciles a Gateway object +type ReconcileGateway struct { + client.Client + scheme *runtime.Scheme + recorder record.EventRecorder + Configration config.GatewayPickupControllerConfiguration +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { + return &ReconcileGateway{ + Client: utilclient.NewClientFromManager(mgr, common.GatewayPickupControllerName), + scheme: mgr.GetScheme(), + recorder: mgr.GetEventRecorderFor(common.GatewayPickupControllerName), + Configration: c.ComponentConfig.GatewayPickupController, + } +} + +// add is used to add a new Controller to mgr +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New(common.GatewayPickupControllerName, mgr, controller.Options{ + Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, + }) + if err != nil { + return err + } + + // Watch for changes to Gateway + err = c.Watch(&source.Kind{Type: &ravenv1beta1.Gateway{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // Watch for changes to Nodes + err = c.Watch(&source.Kind{Type: &corev1.Node{}}, &EnqueueGatewayForNode{}) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &EnqueueGatewayForRavenConfig{client: utilclient.NewClientFromManager(mgr, "raven-config")}, predicate.NewPredicateFuncs( + func(object client.Object) bool { + cm, ok := object.(*corev1.ConfigMap) + if !ok { + return false + } + if cm.GetNamespace() != utils.WorkingNamespace { + return false + } + if cm.GetName() != utils.RavenGlobalConfig { + return false + } + return true + })) + if err != nil { + return err + } + return nil +} + +//+kubebuilder:rbac:groups=raven.openyurt.io,resources=gateways,verbs=get;list;watch;create;delete;update +//+kubebuilder:rbac:groups=raven.openyurt.io,resources=gateways/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=raven.openyurt.io,resources=gateways/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=endpoints,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=crd.projectcalico.org,resources=blockaffinities,verbs=get;list;watch + +// Reconcile reads that state of the cluster for a Gateway object and makes changes based on the state read +// and what is in the Gateway.Spec +func (r *ReconcileGateway) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + + // Note !!!!!!!!!! + // We strongly recommend use Format() to encapsulation because Format() can print logs by module + // @kadisi + klog.V(2).Info(Format("started reconciling Gateway %s/%s", req.Namespace, req.Name)) + defer func() { + klog.V(2).Info(Format("finished reconciling Gateway %s/%s", req.Namespace, req.Name)) + }() + + var gw ravenv1beta1.Gateway + if err := r.Get(ctx, req.NamespacedName, &gw); err != nil { + return reconcile.Result{}, client.IgnoreNotFound(err) + } + + // get all managed nodes + var nodeList corev1.NodeList + nodeSelector, err := labels.Parse(fmt.Sprintf(raven.LabelCurrentGateway+"=%s", gw.Name)) + if err != nil { + return reconcile.Result{}, err + } + err = r.List(ctx, &nodeList, &client.ListOptions{ + LabelSelector: nodeSelector, + }) + if err != nil { + err = fmt.Errorf("unable to list nodes: %s", err) + return reconcile.Result{}, err + } + klog.V(1).Info(Format("list gateway %d node %v", len(nodeList.Items), nodeList.Items)) + // 1. try to elect an active endpoint if possible + activeEp := r.electActiveEndpoint(nodeList, &gw) + r.recordEndpointEvent(&gw, gw.Status.ActiveEndpoints, activeEp) + gw.Status.ActiveEndpoints = activeEp + // 2. get nodeInfo list of nodes managed by the Gateway + var nodes []ravenv1beta1.NodeInfo + for _, v := range nodeList.Items { + podCIDRs, err := r.getPodCIDRs(ctx, v) + if err != nil { + klog.ErrorS(err, "unable to get podCIDR") + return reconcile.Result{}, err + } + nodes = append(nodes, ravenv1beta1.NodeInfo{ + NodeName: v.Name, + PrivateIP: utils.GetNodeInternalIP(v), + Subnets: podCIDRs, + }) + } + sort.Slice(nodes, func(i, j int) bool { return nodes[i].NodeName < nodes[j].NodeName }) + klog.V(4).Info(Format("managed node info list, nodes: %v", nodes)) + gw.Status.Nodes = nodes + err = r.Status().Update(ctx, &gw) + if err != nil { + if apierrs.IsConflict(err) { + klog.Warning(err, Format("unable to update gateway.status, error %s", err.Error())) + return reconcile.Result{Requeue: true, RequeueAfter: 5 * time.Second}, nil + } + return reconcile.Result{Requeue: true, RequeueAfter: 5 * time.Second}, + fmt.Errorf("unable to update %s gateway.status, error %s", gw.GetName(), err.Error()) + } + return reconcile.Result{}, nil +} + +func (r *ReconcileGateway) recordEndpointEvent(sourceObj *ravenv1beta1.Gateway, previous, current []*ravenv1beta1.Endpoint) { + sort.Slice(previous, func(i, j int) bool { return previous[i].NodeName < previous[j].NodeName }) + sort.Slice(current, func(i, j int) bool { return current[i].NodeName < current[j].NodeName }) + if len(current) != 0 && !reflect.DeepEqual(previous, current) { + eps, num := getActiveEndpointsInfo(current) + for i := 0; i < num; i++ { + r.recorder.Event(sourceObj.DeepCopy(), corev1.EventTypeNormal, + ravenv1beta1.EventActiveEndpointElected, + fmt.Sprintf("The endpoint hosted by node %s has been elected active endpoint, publicIP: %s, type: %s", eps[ActiveEndpointsName][i], eps[ActiveEndpointsPublicIP][i], eps[ActiveEndpointsType][i])) + } + + klog.V(2).InfoS(Format("elected new active endpoint"), "nodeName", eps[ActiveEndpointsName], "publicIP", eps[ActiveEndpointsPublicIP], "type", eps[ActiveEndpointsType]) + return + } + if len(previous) != 0 && !reflect.DeepEqual(previous, current) { + eps, num := getActiveEndpointsInfo(previous) + for i := 0; i < num; i++ { + r.recorder.Event(sourceObj.DeepCopy(), corev1.EventTypeWarning, + ravenv1beta1.EventActiveEndpointLost, + fmt.Sprintf("The active endpoint hosted by node %s was change, publicIP: %s, type :%s", eps[ActiveEndpointsName][i], eps[ActiveEndpointsPublicIP][i], eps[ActiveEndpointsType][i])) + } + klog.V(2).InfoS(Format("active endpoint lost"), "nodeName", eps[ActiveEndpointsName], "publicIP", eps[ActiveEndpointsPublicIP], "type", eps[ActiveEndpointsType]) + return + } +} + +// electActiveEndpoint trys to elect an active Endpoint. +// If the current active endpoint remains valid, then we don't change it. +// Otherwise, try to elect a new one. +func (r *ReconcileGateway) electActiveEndpoint(nodeList corev1.NodeList, gw *ravenv1beta1.Gateway) []*ravenv1beta1.Endpoint { + // get all ready nodes referenced by endpoints + readyNodes := make(map[string]*corev1.Node) + for _, v := range nodeList.Items { + if isNodeReady(v) { + readyNodes[v.Name] = &v + } + } + klog.V(1).Infof(Format("Ready node has %d, node %v", len(readyNodes), readyNodes)) + // init a endpoints slice + enableProxy, enableTunnel := utils.CheckServer(context.TODO(), r.Client) + eps := make([]*ravenv1beta1.Endpoint, 0) + if enableProxy { + eps = append(eps, electEndpoints(gw, ravenv1beta1.Proxy, readyNodes)...) + } + if enableTunnel { + eps = append(eps, electEndpoints(gw, ravenv1beta1.Tunnel, readyNodes)...) + } + sort.Slice(eps, func(i, j int) bool { return eps[i].NodeName < eps[j].NodeName }) + return eps +} + +func electEndpoints(gw *ravenv1beta1.Gateway, endpointType string, readyNodes map[string]*corev1.Node) []*ravenv1beta1.Endpoint { + eps := make([]*ravenv1beta1.Endpoint, 0) + var replicas int + switch endpointType { + case ravenv1beta1.Proxy: + replicas = gw.Spec.ProxyConfig.Replicas + case ravenv1beta1.Tunnel: + replicas = gw.Spec.TunnelConfig.Replicas + default: + replicas = 1 + } + + checkCandidates := func(ep *ravenv1beta1.Endpoint) bool { + if _, ok := readyNodes[ep.NodeName]; ok && ep.Type == endpointType { + return true + } + return false + } + + // the current active endpoint is still competent. + candidates := make(map[string]*ravenv1beta1.Endpoint, 0) + for _, activeEndpoint := range gw.Status.ActiveEndpoints { + if checkCandidates(activeEndpoint) { + for _, ep := range gw.Spec.Endpoints { + if ep.NodeName == activeEndpoint.NodeName && ep.Type == activeEndpoint.Type { + candidates[activeEndpoint.NodeName] = ep.DeepCopy() + } + } + } + } + for _, aep := range candidates { + if len(eps) == replicas { + aepInfo, _ := getActiveEndpointsInfo(eps) + klog.V(4).InfoS(Format("elect %d active endpoints %s for gateway %s/%s", + len(eps), fmt.Sprintf("[%s]", strings.Join(aepInfo[ActiveEndpointsName], ",")), gw.GetNamespace(), gw.GetName())) + return eps + } + klog.V(1).Infof(Format("node %s is active endpoints, type is %s", aep.NodeName, aep.Type)) + klog.V(1).Infof(Format("add node %v", aep.DeepCopy())) + eps = append(eps, aep.DeepCopy()) + } + + for _, ep := range gw.Spec.Endpoints { + if _, ok := candidates[ep.NodeName]; !ok && checkCandidates(&ep) { + if len(eps) == replicas { + aepInfo, _ := getActiveEndpointsInfo(eps) + klog.V(4).InfoS(Format("elect %d active endpoints %s for gateway %s/%s", + len(eps), fmt.Sprintf("[%s]", strings.Join(aepInfo[ActiveEndpointsName], ",")), gw.GetNamespace(), gw.GetName())) + return eps + } + klog.V(1).Infof(Format("node %s is active endpoints, type is %s", ep.NodeName, ep.Type)) + klog.V(1).Infof(Format("add node %v", ep.DeepCopy())) + eps = append(eps, ep.DeepCopy()) + } + } + return eps +} + +// isNodeReady checks if the `node` is `corev1.NodeReady` +func isNodeReady(node corev1.Node) bool { + _, nc := nodeutil.GetNodeCondition(&node.Status, corev1.NodeReady) + // GetNodeCondition will return nil and -1 if the condition is not present + return nc != nil && nc.Status == corev1.ConditionTrue +} + +// getPodCIDRs returns the pod IP ranges assigned to the node. +func (r *ReconcileGateway) getPodCIDRs(ctx context.Context, node corev1.Node) ([]string, error) { + podCIDRs := make([]string, 0) + for key := range node.Annotations { + if strings.Contains(key, "projectcalico.org") { + var blockAffinityList calicov3.BlockAffinityList + err := r.List(ctx, &blockAffinityList) + if err != nil { + err = fmt.Errorf(Format("unable to list calico blockaffinity: %s", err)) + return nil, err + } + for _, v := range blockAffinityList.Items { + if v.Spec.Node != node.Name || v.Spec.State != "confirmed" { + continue + } + podCIDRs = append(podCIDRs, v.Spec.CIDR) + } + return podCIDRs, nil + } + } + return append(podCIDRs, node.Spec.PodCIDR), nil +} + +func getActiveEndpointsInfo(eps []*ravenv1beta1.Endpoint) (map[string][]string, int) { + infos := make(map[string][]string) + infos[ActiveEndpointsName] = make([]string, 0) + infos[ActiveEndpointsPublicIP] = make([]string, 0) + infos[ActiveEndpointsType] = make([]string, 0) + if len(eps) == 0 { + return infos, 0 + } + for _, ep := range eps { + infos[ActiveEndpointsName] = append(infos[ActiveEndpointsName], ep.NodeName) + infos[ActiveEndpointsPublicIP] = append(infos[ActiveEndpointsPublicIP], ep.PublicIP) + infos[ActiveEndpointsType] = append(infos[ActiveEndpointsType], ep.Type) + } + return infos, len(infos[ActiveEndpointsName]) +} diff --git a/pkg/controller/raven/gatewaypickup/gateway_pickup_controller_test.go b/pkg/controller/raven/gatewaypickup/gateway_pickup_controller_test.go new file mode 100644 index 00000000000..c6586a2cc17 --- /dev/null +++ b/pkg/controller/raven/gatewaypickup/gateway_pickup_controller_test.go @@ -0,0 +1,467 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewaypickup + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" + "github.com/openyurtio/openyurt/pkg/controller/raven/utils" +) + +var ( + nodeReadyStatus = corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + } + nodeNotReadyStatus = corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionFalse, + }, + }, + } +) + +func TestReconcileGateway_electActiveEndpoint(t *testing.T) { + obj := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.RavenGlobalConfig, + Namespace: utils.WorkingNamespace, + }, + Data: map[string]string{ + utils.RavenEnableProxy: "true", + utils.RavenEnableTunnel: "true", + }, + } + + mockReconciler := &ReconcileGateway{ + Configration: config.GatewayPickupControllerConfiguration{}, + Client: fake.NewClientBuilder().WithObjects(obj).Build(), + } + var tt = []struct { + name string + nodeList corev1.NodeList + gw *ravenv1beta1.Gateway + expectedEps []*ravenv1beta1.Endpoint + }{ + + { + // The node hosting active endpoint becomes NotReady, and it is the only node in the Gateway, + // then the active endpoint should be removed. + name: "lost active endpoint", + nodeList: corev1.NodeList{ + Items: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + Status: nodeNotReadyStatus, + }, + }, + }, + gw: &ravenv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-1", + }, + Spec: ravenv1beta1.GatewaySpec{ + ProxyConfig: ravenv1beta1.ProxyConfiguration{ + Replicas: 1, + }, + TunnelConfig: ravenv1beta1.TunnelConfiguration{ + Replicas: 1, + }, + Endpoints: []ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-1", + Type: ravenv1beta1.Proxy, + }, + }, + }, + Status: ravenv1beta1.GatewayStatus{ + ActiveEndpoints: []*ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-1", + Type: ravenv1beta1.Proxy, + }, + }, + }, + }, + expectedEps: []*ravenv1beta1.Endpoint{}, + }, + { + // The node hosting active endpoint becomes NotReady, but there are at least one Ready node, + // then a new endpoint should be elected active endpoint to replace the old one. + name: "switch active endpoint", + nodeList: corev1.NodeList{ + Items: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + Status: nodeNotReadyStatus, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-2", + }, + Status: nodeReadyStatus, + }, + }, + }, + gw: &ravenv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-1", + }, + Spec: ravenv1beta1.GatewaySpec{ + ProxyConfig: ravenv1beta1.ProxyConfiguration{ + Replicas: 2, + }, + TunnelConfig: ravenv1beta1.TunnelConfiguration{ + Replicas: 1, + }, + Endpoints: []ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-1", + Type: ravenv1beta1.Proxy, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Proxy, + }, + }, + }, + Status: ravenv1beta1.GatewayStatus{ + ActiveEndpoints: []*ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-1", + Type: ravenv1beta1.Proxy, + }, + }, + }, + }, + expectedEps: []*ravenv1beta1.Endpoint{ + { + NodeName: "node-2", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Proxy, + }, + }, + }, + + { + name: "elect new active endpoint", + nodeList: corev1.NodeList{ + Items: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + Status: nodeNotReadyStatus, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-2", + }, + Status: nodeReadyStatus, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-3", + }, + Status: nodeReadyStatus, + }, + }, + }, + gw: &ravenv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-1", + }, + Spec: ravenv1beta1.GatewaySpec{ + ProxyConfig: ravenv1beta1.ProxyConfiguration{ + Replicas: 2, + }, + TunnelConfig: ravenv1beta1.TunnelConfiguration{ + Replicas: 1, + }, + Endpoints: []ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Proxy, + }, + { + NodeName: "node-3", + Type: ravenv1beta1.Proxy, + }, + }, + }, + Status: ravenv1beta1.GatewayStatus{ + ActiveEndpoints: []*ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Proxy, + }, + }, + }, + }, + expectedEps: []*ravenv1beta1.Endpoint{ + { + NodeName: "node-2", + Type: ravenv1beta1.Proxy, + }, + { + NodeName: "node-3", + Type: ravenv1beta1.Proxy, + }, + }, + }, + + { + name: "no available active endpoint", + nodeList: corev1.NodeList{ + Items: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + Status: nodeNotReadyStatus, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-2", + }, + Status: nodeNotReadyStatus, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-3", + }, + Status: nodeNotReadyStatus, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-4", + }, + Status: nodeNotReadyStatus, + }, + }, + }, + gw: &ravenv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-1", + }, + Spec: ravenv1beta1.GatewaySpec{ + ProxyConfig: ravenv1beta1.ProxyConfiguration{ + Replicas: 2, + }, + TunnelConfig: ravenv1beta1.TunnelConfiguration{ + Replicas: 1, + }, + Endpoints: []ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-3", + Type: ravenv1beta1.Proxy, + }, + { + NodeName: "node-4", + Type: ravenv1beta1.Proxy, + }, + }, + }, + Status: ravenv1beta1.GatewayStatus{ + ActiveEndpoints: []*ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-3", + Type: ravenv1beta1.Proxy, + }, + { + NodeName: "node-4", + Type: ravenv1beta1.Proxy, + }, + }, + }, + }, + expectedEps: []*ravenv1beta1.Endpoint{}, + }, + + { + // The node hosting the active endpoint is still ready, do not change it. + name: "don't switch active endpoint", + nodeList: corev1.NodeList{ + Items: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + Status: nodeNotReadyStatus, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-2", + }, + Status: nodeReadyStatus, + }, + }, + }, + gw: &ravenv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway-1", + }, + Spec: ravenv1beta1.GatewaySpec{ + ProxyConfig: ravenv1beta1.ProxyConfiguration{ + Replicas: 1, + }, + TunnelConfig: ravenv1beta1.TunnelConfiguration{ + Replicas: 1, + }, + Endpoints: []ravenv1beta1.Endpoint{ + { + NodeName: "node-1", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Proxy, + }, + }, + }, + Status: ravenv1beta1.GatewayStatus{ + ActiveEndpoints: []*ravenv1beta1.Endpoint{ + { + NodeName: "node-2", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Proxy, + }, + }, + }, + }, + expectedEps: []*ravenv1beta1.Endpoint{ + { + NodeName: "node-2", + Type: ravenv1beta1.Tunnel, + }, + { + NodeName: "node-2", + Type: ravenv1beta1.Proxy, + }, + }, + }, + } + for _, v := range tt { + t.Run(v.name, func(t *testing.T) { + a := assert.New(t) + eps := mockReconciler.electActiveEndpoint(v.nodeList, v.gw) + a.Equal(len(v.expectedEps), len(eps)) + }) + } + +} + +func TestReconcileGateway_getPodCIDRs(t *testing.T) { + mockReconciler := &ReconcileGateway{ + Configration: config.GatewayPickupControllerConfiguration{}, + } + var tt = []struct { + name string + node corev1.Node + expectPodCIDR []string + }{ + { + name: "node has pod CIDR", + node: corev1.Node{ + Spec: corev1.NodeSpec{ + PodCIDR: "10.0.0.1/24", + }, + }, + expectPodCIDR: []string{"10.0.0.1/24"}, + }, + { + name: "node hasn't pod CIDR", + node: corev1.Node{ + Spec: corev1.NodeSpec{}, + }, + expectPodCIDR: []string{""}, + }, + } + for _, v := range tt { + t.Run(v.name, func(t *testing.T) { + a := assert.New(t) + podCIDRs, err := mockReconciler.getPodCIDRs(context.Background(), v.node) + if a.NoError(err) { + a.Equal(v.expectPodCIDR, podCIDRs) + } + }) + } +} diff --git a/pkg/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go b/pkg/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go new file mode 100644 index 00000000000..26c57e14847 --- /dev/null +++ b/pkg/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go @@ -0,0 +1,179 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewaypickup + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + + "github.com/openyurtio/openyurt/pkg/apis/raven" + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + "github.com/openyurtio/openyurt/pkg/controller/raven/utils" +) + +type EnqueueGatewayForNode struct{} + +// Create implements EventHandler +func (e *EnqueueGatewayForNode) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) { + node, ok := evt.Object.(*corev1.Node) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1.Node")) + return + } + klog.V(5).Infof(Format("will enqueue gateway as node(%s) has been created", + node.GetName())) + if gwName, exist := node.Labels[raven.LabelCurrentGateway]; exist { + utils.AddGatewayToWorkQueue(gwName, q) + return + } + klog.V(4).Infof(Format("node(%s) does not belong to any gateway", node.GetName())) +} + +// Update implements EventHandler +func (e *EnqueueGatewayForNode) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) { + newNode, ok := evt.ObjectNew.(*corev1.Node) + if !ok { + klog.Errorf(Format("Fail to assert runtime Object(%s) to v1.Node", + evt.ObjectNew.GetName())) + return + } + oldNode, ok := evt.ObjectOld.(*corev1.Node) + if !ok { + klog.Errorf(Format("fail to assert runtime Object(%s) to v1.Node", + evt.ObjectOld.GetName())) + return + } + klog.V(5).Infof(Format("Will enqueue gateway as node(%s) has been updated", + newNode.GetName())) + + oldGwName := oldNode.Labels[raven.LabelCurrentGateway] + newGwName := newNode.Labels[raven.LabelCurrentGateway] + + // check if NodeReady condition changed + statusChanged := func(oldObj, newObj *corev1.Node) bool { + return isNodeReady(*oldObj) != isNodeReady(*newObj) + } + + if oldGwName != newGwName || statusChanged(oldNode, newNode) { + utils.AddGatewayToWorkQueue(oldGwName, q) + utils.AddGatewayToWorkQueue(newGwName, q) + } +} + +// Delete implements EventHandler +func (e *EnqueueGatewayForNode) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) { + node, ok := evt.Object.(*corev1.Node) + if !ok { + klog.Error(Format("Fail to assert runtime Object to v1.Node")) + return + } + + gwName, exist := node.Labels[raven.LabelCurrentGateway] + if !exist { + klog.V(5).Infof(Format("Node(%s) doesn't belong to any gateway", node.GetName())) + return + } + // enqueue the gateway that the node belongs to + klog.V(5).Infof(Format("Will enqueue pool(%s) as node(%s) has been deleted", + gwName, node.GetName())) + utils.AddGatewayToWorkQueue(gwName, q) +} + +// Generic implements EventHandler +func (e *EnqueueGatewayForNode) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) { +} + +type EnqueueGatewayForRavenConfig struct { + client client.Client +} + +func (e *EnqueueGatewayForRavenConfig) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) { + _, ok := evt.Object.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("Fail to assert runtime Object to v1.ConfigMap")) + return + } + klog.V(2).Infof(Format("Will config all gateway as raven-cfg has been created")) + if err := e.enqueueGateways(q); err != nil { + klog.Error(Format("failed to config all gateway, error %s", err.Error())) + return + } +} + +func (e *EnqueueGatewayForRavenConfig) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) { + oldCm, ok := evt.ObjectOld.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("Fail to assert runtime Object to v1.ConfigMap")) + return + } + + newCm, ok := evt.ObjectNew.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("Fail to assert runtime Object to v1.ConfigMap")) + return + } + + if oldCm.Data[utils.RavenEnableProxy] != newCm.Data[utils.RavenEnableProxy] { + klog.V(2).Infof(Format("Will config all gateway as raven-cfg has been updated")) + if err := e.enqueueGateways(q); err != nil { + klog.Error(Format("failed to config all gateway, error %s", err.Error())) + return + } + } + + if oldCm.Data[utils.RavenEnableTunnel] != newCm.Data[utils.RavenEnableTunnel] { + klog.V(2).Infof(Format("Will config all gateway as raven-cfg has been updated")) + if err := e.enqueueGateways(q); err != nil { + klog.Error(Format("failed to config all gateway, error %s", err.Error())) + return + } + } +} + +func (e *EnqueueGatewayForRavenConfig) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) { + _, ok := evt.Object.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("Fail to assert runtime Object to v1.ConfigMap")) + return + } + klog.V(2).Infof(Format("Will config all gateway as raven-cfg has been deleted")) + if err := e.enqueueGateways(q); err != nil { + klog.Error(Format("failed to config all gateway, error %s", err.Error())) + return + } +} + +func (e *EnqueueGatewayForRavenConfig) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) { + +} + +func (e *EnqueueGatewayForRavenConfig) enqueueGateways(q workqueue.RateLimitingInterface) error { + var gwList ravenv1beta1.GatewayList + err := e.client.List(context.TODO(), &gwList) + if err != nil { + return err + } + for _, gw := range gwList.Items { + utils.AddGatewayToWorkQueue(gw.Name, q) + } + return nil +} diff --git a/pkg/controller/raven/service/service_controller.go b/pkg/controller/raven/service/service_controller.go deleted file mode 100644 index 5dd811591ad..00000000000 --- a/pkg/controller/raven/service/service_controller.go +++ /dev/null @@ -1,303 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the License); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an AS IS BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package service - -import ( - "context" - "fmt" - "reflect" - - corev1 "k8s.io/api/core/v1" - apierrs "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/client-go/tools/record" - "k8s.io/klog/v2" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/controller-runtime/pkg/source" - - appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - ravenv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1alpha1" - common "github.com/openyurtio/openyurt/pkg/controller/raven" - "github.com/openyurtio/openyurt/pkg/controller/raven/utils" - utilclient "github.com/openyurtio/openyurt/pkg/util/client" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" -) - -var ( - controllerKind = corev1.SchemeGroupVersion.WithKind("Service") -) - -func Format(format string, args ...interface{}) string { - s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s-service: %s", common.ControllerName, s) -} - -// Add creates a new Service Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller -// and Start it when the Manager is Started. -func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - return nil - } - - klog.Infof("ravenl3-service-controller add controller %s", controllerKind.String()) - return add(mgr, newReconciler(c, mgr)) -} - -var _ reconcile.Reconciler = &ReconcileService{} - -// ReconcileService reconciles a Gateway object -type ReconcileService struct { - client.Client - scheme *runtime.Scheme - recorder record.EventRecorder -} - -// newReconciler returns a new reconcile.Reconciler -func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { - return &ReconcileService{ - Client: utilclient.NewClientFromManager(mgr, common.ControllerName), - scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(common.ControllerName), - } -} - -// add adds a new Controller to mgr with r as the reconcile.Reconciler -func add(mgr manager.Manager, r reconcile.Reconciler) error { - // Create a new controller - c, err := controller.New(fmt.Sprintf("%s-service", common.ControllerName), mgr, controller.Options{ - Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, - }) - if err != nil { - return err - } - - // Watch for changes to Service - err = c.Watch(&source.Kind{Type: &corev1.Service{}}, handler.EnqueueRequestsFromMapFunc(mapServiceToRequest)) - if err != nil { - return err - } - // Watch for changes to Gateway - err = c.Watch(&source.Kind{Type: &ravenv1alpha1.Gateway{}}, handler.EnqueueRequestsFromMapFunc(mapGatewayToRequest)) - if err != nil { - return err - } - - // Watch for changes to Endpoints - err = c.Watch(&source.Kind{Type: &corev1.Endpoints{}}, handler.EnqueueRequestsFromMapFunc(mapEndpointToRequest)) - if err != nil { - return err - } - - return nil -} - -// Reconcile reads that state of the cluster for a Gateway object and makes changes based on the state read -// and what is in the Gateway.Spec -func (r *ReconcileService) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { - klog.V(4).Info(Format("started reconciling Service %s/%s", req.Namespace, req.Name)) - defer func() { - klog.V(4).Info(Format("finished reconciling Service %s/%s", req.Namespace, req.Name)) - }() - - var gatewayList ravenv1alpha1.GatewayList - if err := r.List(ctx, &gatewayList); err != nil { - err = fmt.Errorf(Format("unable to list gateways: %s", err)) - return reconcile.Result{}, err - } - - if err := r.reconcileService(ctx, req, &gatewayList); err != nil { - err = fmt.Errorf(Format("unable to reconcile service: %s", err)) - return reconcile.Result{}, err - } - - if err := r.reconcileEndpoint(ctx, req, &gatewayList); err != nil { - err = fmt.Errorf(Format("unable to reconcile endpoint: %s", err)) - return reconcile.Result{}, err - } - return reconcile.Result{}, nil -} - -func (r *ReconcileService) reconcileService(ctx context.Context, req ctrl.Request, gatewayList *ravenv1alpha1.GatewayList) error { - for _, gw := range gatewayList.Items { - if utils.IsGatewayExposeByLB(&gw) { - return r.ensureService(ctx, req) - } - } - return r.cleanService(ctx, req) -} - -func (r *ReconcileService) cleanService(ctx context.Context, req ctrl.Request) error { - if err := r.Delete(ctx, &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: req.Name, - Namespace: req.Namespace, - }, - }); err != nil && !apierrs.IsNotFound(err) { - return err - } - return nil -} - -func (r *ReconcileService) ensureService(ctx context.Context, req ctrl.Request) error { - var service corev1.Service - if err := r.Get(ctx, req.NamespacedName, &service); err != nil { - if apierrs.IsNotFound(err) { - klog.V(2).InfoS(Format("create service"), "name", req.Name, "namespace", req.Namespace) - return r.Create(ctx, &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: req.Name, - Namespace: req.Namespace, - }, - Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{ - { - Port: 4500, - Protocol: corev1.ProtocolUDP, - TargetPort: intstr.FromInt(4500), - }, - }, - Type: corev1.ServiceTypeLoadBalancer, - ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal, - }, - }) - - } - } - return nil -} - -func (r *ReconcileService) reconcileEndpoint(ctx context.Context, req ctrl.Request, gatewayList *ravenv1alpha1.GatewayList) error { - exposedByLB := false - for _, gw := range gatewayList.Items { - if utils.IsGatewayExposeByLB(&gw) { - exposedByLB = true - if gw.Status.ActiveEndpoint != nil { - var node corev1.Node - if err := r.Get(ctx, types.NamespacedName{ - Name: gw.Status.ActiveEndpoint.NodeName, - }, &node); err != nil { - return err - } - return r.ensureEndpoint(ctx, req, node) - } - } - } - if !exposedByLB { - return r.cleanEndpoint(ctx, req) - } - return nil -} - -func (r *ReconcileService) cleanEndpoint(ctx context.Context, req ctrl.Request) error { - if err := r.Delete(ctx, &corev1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{ - Name: req.Name, - Namespace: req.Namespace, - }, - }); err != nil && !apierrs.IsNotFound(err) { - return err - } - return nil -} - -func (r *ReconcileService) ensureEndpoint(ctx context.Context, req ctrl.Request, node corev1.Node) error { - var serviceEndpoint corev1.Endpoints - newSubnets := []corev1.EndpointSubset{ - { - Addresses: []corev1.EndpointAddress{ - { - IP: utils.GetNodeInternalIP(node), - NodeName: func(n corev1.Node) *string { return &n.Name }(node), - }, - }, - Ports: []corev1.EndpointPort{ - { - Port: 4500, - Protocol: corev1.ProtocolUDP, - }, - }, - }, - } - if err := r.Get(ctx, req.NamespacedName, &serviceEndpoint); err != nil { - if apierrs.IsNotFound(err) { - klog.V(2).InfoS(Format("create endpoint"), "name", req.Name, "namespace", req.Namespace) - return r.Create(ctx, &corev1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{ - Name: req.Name, - Namespace: req.Namespace, - }, - Subsets: newSubnets, - }) - } - return err - } - - if !reflect.DeepEqual(serviceEndpoint.Subsets, newSubnets) { - klog.V(2).InfoS(Format("update endpoint"), "name", req.Name, "namespace", req.Namespace) - serviceEndpoint.Subsets = newSubnets - return r.Update(ctx, &serviceEndpoint) - } - klog.V(2).InfoS(Format("skip to update endpoint"), "name", req.Name, "namespace", req.Namespace) - return nil -} - -// mapGatewayToRequest maps the given Gateway object to reconcile.Request. -func mapGatewayToRequest(object client.Object) []reconcile.Request { - gw, ok := object.(*ravenv1alpha1.Gateway) - if ok && utils.IsGatewayExposeByLB(gw) { - return []reconcile.Request{ - { - NamespacedName: ravenv1alpha1.ServiceNamespacedName, - }, - } - } - return []reconcile.Request{} -} - -// mapEndpointToRequest maps the given Endpoint object to reconcile.Request. -func mapEndpointToRequest(object client.Object) []reconcile.Request { - ep, ok := object.(*corev1.Endpoints) - if ok && ep.Name == ravenv1alpha1.ServiceNamespacedName.Name && ep.Namespace == ravenv1alpha1.ServiceNamespacedName.Namespace { - return []reconcile.Request{ - { - NamespacedName: ravenv1alpha1.ServiceNamespacedName, - }, - } - } - return []reconcile.Request{} -} - -// mapEndpointToRequest maps the given Endpoint object to reconcile.Request. -func mapServiceToRequest(object client.Object) []reconcile.Request { - svc, ok := object.(*corev1.Service) - if ok && svc.Name == ravenv1alpha1.ServiceNamespacedName.Name && svc.Namespace == ravenv1alpha1.ServiceNamespacedName.Namespace { - return []reconcile.Request{ - { - NamespacedName: ravenv1alpha1.ServiceNamespacedName, - }, - } - } - return []reconcile.Request{} -} diff --git a/pkg/controller/raven/utils/utils.go b/pkg/controller/raven/utils/utils.go index 71609f083b9..0bb880c42f0 100644 --- a/pkg/controller/raven/utils/utils.go +++ b/pkg/controller/raven/utils/utils.go @@ -17,16 +17,27 @@ limitations under the License. package utils import ( + "context" "net" + "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/openyurtio/openyurt/pkg/apis/raven/v1alpha1" ) +const ( + WorkingNamespace = "kube-system" + RavenGlobalConfig = "raven-cfg" + + RavenEnableProxy = "EnableL7Proxy" + RavenEnableTunnel = "EnableL3Tunnel" +) + // GetNodeInternalIP returns internal ip of the given `node`. func GetNodeInternalIP(node corev1.Node) string { var ip string @@ -52,3 +63,21 @@ func AddGatewayToWorkQueue(gwName string, }) } } + +func CheckServer(ctx context.Context, client client.Client) (enableProxy, enableTunnel bool) { + var cm corev1.ConfigMap + enableTunnel = false + enableProxy = false + err := client.Get(ctx, types.NamespacedName{Namespace: WorkingNamespace, Name: RavenGlobalConfig}, &cm) + if err != nil { + return enableProxy, enableTunnel + } + if val, ok := cm.Data[RavenEnableProxy]; ok && strings.ToLower(val) == "true" { + enableProxy = true + } + if val, ok := cm.Data[RavenEnableTunnel]; ok && strings.ToLower(val) == "true" { + enableTunnel = true + } + return enableProxy, enableTunnel + +} From 4bd641459e5bba87f0875fb99f029d7d00e5f645 Mon Sep 17 00:00:00 2001 From: rambohe Date: Thu, 17 Aug 2023 12:13:23 +0800 Subject: [PATCH 77/93] remove some client global vars (#1655) Signed-off-by: rambohe-ch --- cmd/yurt-manager/app/manager.go | 9 -- pkg/client/generic_client.go | 57 ------------ pkg/client/registry.go | 56 ------------ .../daemon_pod_updater_controller.go | 7 +- .../platformadmin/platformadmin_controller.go | 13 ++- .../gateway_pickup_controller.go | 15 ++- .../endpoints/endpoints_controller.go | 5 - .../yurtappdaemon/yurtappdaemon_controller.go | 11 +-- .../yurtappset/yurtappset_controller.go | 13 +-- .../yurtstaticset/yurtstaticset_controller.go | 13 ++- pkg/profile/profile_test.go | 53 ++++++++--- pkg/util/client/client.go | 34 ------- pkg/util/discovery/discovery.go | 91 ------------------- .../util/controller/webhook_controller.go | 7 +- 14 files changed, 74 insertions(+), 310 deletions(-) delete mode 100644 pkg/client/generic_client.go delete mode 100644 pkg/client/registry.go delete mode 100644 pkg/util/client/client.go delete mode 100644 pkg/util/discovery/discovery.go diff --git a/cmd/yurt-manager/app/manager.go b/cmd/yurt-manager/app/manager.go index fd328f28674..f615dfd103a 100644 --- a/cmd/yurt-manager/app/manager.go +++ b/cmd/yurt-manager/app/manager.go @@ -37,7 +37,6 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/options" "github.com/openyurtio/openyurt/pkg/apis" - extclient "github.com/openyurtio/openyurt/pkg/client" "github.com/openyurtio/openyurt/pkg/controller" "github.com/openyurtio/openyurt/pkg/profile" "github.com/openyurtio/openyurt/pkg/projectinfo" @@ -151,16 +150,8 @@ func Run(c *config.CompletedConfig, stopCh <-chan struct{}) error { ctx := ctrl.SetupSignalHandler() cfg := ctrl.GetConfigOrDie() - setRestConfig(cfg, c) - setupLog.Info("new clientset registry") - err := extclient.NewRegistry(cfg) - if err != nil { - setupLog.Error(err, "unable to init yurt-manager clientset and informer") - os.Exit(1) - } - mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme, MetricsBindAddress: c.ComponentConfig.Generic.MetricsAddr, diff --git a/pkg/client/generic_client.go b/pkg/client/generic_client.go deleted file mode 100644 index 1ee4a106d8d..00000000000 --- a/pkg/client/generic_client.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package client - -import ( - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/discovery" - kubeclientset "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" -) - -// GenericClientset defines a generic client -type GenericClientset struct { - DiscoveryClient discovery.DiscoveryInterface - KubeClient kubeclientset.Interface -} - -// newForConfig creates a new Clientset for the given config. -func newForConfig(c *rest.Config) (*GenericClientset, error) { - cWithProtobuf := rest.CopyConfig(c) - cWithProtobuf.ContentType = runtime.ContentTypeProtobuf - discoveryClient, err := discovery.NewDiscoveryClientForConfig(cWithProtobuf) - if err != nil { - return nil, err - } - kubeClient, err := kubeclientset.NewForConfig(cWithProtobuf) - if err != nil { - return nil, err - } - return &GenericClientset{ - DiscoveryClient: discoveryClient, - KubeClient: kubeClient, - }, nil -} - -// newForConfig creates a new Clientset for the given config. -func newForConfigOrDie(c *rest.Config) *GenericClientset { - gc, err := newForConfig(c) - if err != nil { - panic(err) - } - return gc -} diff --git a/pkg/client/registry.go b/pkg/client/registry.go deleted file mode 100644 index c94aace5eff..00000000000 --- a/pkg/client/registry.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package client - -import ( - "fmt" - - "k8s.io/client-go/rest" -) - -var ( - cfg *rest.Config - - defaultGenericClient *GenericClientset -) - -// NewRegistry creates clientset by client-go -func NewRegistry(c *rest.Config) error { - var err error - defaultGenericClient, err = newForConfig(c) - if err != nil { - return err - } - cfgCopy := *c - cfg = &cfgCopy - return nil -} - -// GetGenericClient returns default clientset -func GetGenericClient() *GenericClientset { - return defaultGenericClient -} - -// GetGenericClientWithName returns clientset with given name as user-agent -func GetGenericClientWithName(name string) *GenericClientset { - if cfg == nil { - return nil - } - newCfg := *cfg - newCfg.UserAgent = fmt.Sprintf("%s/%s", cfg.UserAgent, name) - return newForConfigOrDie(&newCfg) -} diff --git a/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go b/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go index 6c06ea36e19..95e27645a13 100644 --- a/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go +++ b/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go @@ -50,8 +50,6 @@ import ( appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" k8sutil "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater/kubernetes" - utilclient "github.com/openyurtio/openyurt/pkg/util/client" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" ) func init() { @@ -103,9 +101,6 @@ func Format(format string, args ...interface{}) string { // Add creates a new Daemonpodupdater Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - return nil - } klog.Infof("daemonupdater-controller add controller %s", controllerKind.String()) return add(mgr, newReconciler(c, mgr)) } @@ -123,7 +118,7 @@ type ReconcileDaemonpodupdater struct { // newReconciler returns a new reconcile.Reconciler func newReconciler(_ *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { return &ReconcileDaemonpodupdater{ - Client: utilclient.NewClientFromManager(mgr, ControllerName), + Client: mgr.GetClient(), expectations: k8sutil.NewControllerExpectations(), recorder: mgr.GetEventRecorderFor(ControllerName), } diff --git a/pkg/controller/platformadmin/platformadmin_controller.go b/pkg/controller/platformadmin/platformadmin_controller.go index dddb4178a9e..1edc362cfa1 100644 --- a/pkg/controller/platformadmin/platformadmin_controller.go +++ b/pkg/controller/platformadmin/platformadmin_controller.go @@ -49,8 +49,6 @@ import ( iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" util "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" - utilclient "github.com/openyurtio/openyurt/pkg/util/client" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" ) func init() { @@ -64,7 +62,7 @@ func Format(format string, args ...interface{}) string { var ( concurrentReconciles = 3 - controllerKind = iotv1alpha2.SchemeGroupVersion.WithKind("PlatformAdmin") + controllerResource = iotv1alpha2.SchemeGroupVersion.WithResource("platformadmins") ) const ( @@ -121,18 +119,19 @@ var _ reconcile.Reconciler = &ReconcilePlatformAdmin{} // Add creates a new PlatformAdmin Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - return nil + if _, err := mgr.GetRESTMapper().KindFor(controllerResource); err != nil { + klog.Infof("resource %s doesn't exist", controllerResource.String()) + return err } - klog.Infof("platformadmin-controller add controller %s", controllerKind.String()) + klog.Infof("platformadmin-controller add controller %s", controllerResource.String()) return add(mgr, newReconciler(c, mgr)) } // newReconciler returns a new reconcile.Reconciler func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { return &ReconcilePlatformAdmin{ - Client: utilclient.NewClientFromManager(mgr, ControllerName), + Client: mgr.GetClient(), scheme: mgr.GetScheme(), recorder: mgr.GetEventRecorderFor(ControllerName), yamlSerializer: kjson.NewSerializerWithOptions(kjson.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, kjson.SerializerOptions{Yaml: true, Pretty: true}), diff --git a/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go b/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go index 75d26b9477d..24d8337a21a 100644 --- a/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go +++ b/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go @@ -46,12 +46,10 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" "github.com/openyurtio/openyurt/pkg/controller/raven/utils" nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" - utilclient "github.com/openyurtio/openyurt/pkg/util/client" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" ) var ( - controllerKind = ravenv1beta1.SchemeGroupVersion.WithKind("Gateway") + controllerResource = ravenv1beta1.SchemeGroupVersion.WithResource("gateways") ) func Format(format string, args ...interface{}) string { @@ -68,10 +66,11 @@ const ( // Add creates a new Gateway Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - return nil + if _, err := mgr.GetRESTMapper().KindFor(controllerResource); err != nil { + klog.Infof("resource %s doesn't exist", controllerResource.String()) + return err } - klog.Infof("raven-gateway-controller add controller %s", controllerKind.String()) + klog.Infof("raven-gateway-controller add controller %s", controllerResource.String()) return add(mgr, newReconciler(c, mgr)) } @@ -88,7 +87,7 @@ type ReconcileGateway struct { // newReconciler returns a new reconcile.Reconciler func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { return &ReconcileGateway{ - Client: utilclient.NewClientFromManager(mgr, common.GatewayPickupControllerName), + Client: mgr.GetClient(), scheme: mgr.GetScheme(), recorder: mgr.GetEventRecorderFor(common.GatewayPickupControllerName), Configration: c.ComponentConfig.GatewayPickupController, @@ -117,7 +116,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } - err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &EnqueueGatewayForRavenConfig{client: utilclient.NewClientFromManager(mgr, "raven-config")}, predicate.NewPredicateFuncs( + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &EnqueueGatewayForRavenConfig{client: mgr.GetClient()}, predicate.NewPredicateFuncs( func(object client.Object) bool { cm, ok := object.(*corev1.ConfigMap) if !ok { diff --git a/pkg/controller/servicetopology/endpoints/endpoints_controller.go b/pkg/controller/servicetopology/endpoints/endpoints_controller.go index ed1c15ad370..e35426e662f 100644 --- a/pkg/controller/servicetopology/endpoints/endpoints_controller.go +++ b/pkg/controller/servicetopology/endpoints/endpoints_controller.go @@ -33,7 +33,6 @@ import ( appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" common "github.com/openyurtio/openyurt/pkg/controller/servicetopology" "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" ) func init() { @@ -53,10 +52,6 @@ func Format(format string, args ...interface{}) string { // Add creates a new Servicetopology endpoints Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - return nil - } - klog.Infof("servicetopology-endpoints-controller add controller %s", controllerKind.String()) return add(mgr, newReconciler(c, mgr)) } diff --git a/pkg/controller/yurtappdaemon/yurtappdaemon_controller.go b/pkg/controller/yurtappdaemon/yurtappdaemon_controller.go index 9d5cff753f8..0434ec7b70f 100644 --- a/pkg/controller/yurtappdaemon/yurtappdaemon_controller.go +++ b/pkg/controller/yurtappdaemon/yurtappdaemon_controller.go @@ -41,12 +41,11 @@ import ( unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/controller/util" "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon/workloadcontroller" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" ) var ( concurrentReconciles = 3 - controllerKind = unitv1alpha1.SchemeGroupVersion.WithKind("YurtAppDaemon") + controllerResource = unitv1alpha1.SchemeGroupVersion.WithResource("yurtappdaemons") ) const ( @@ -74,12 +73,12 @@ func Format(format string, args ...interface{}) string { // The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(c *config.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - klog.Errorf(Format("DiscoverGVK error")) - return nil + if _, err := mgr.GetRESTMapper().KindFor(controllerResource); err != nil { + klog.Infof("resource %s doesn't exist", controllerResource.String()) + return err } - klog.Infof("yurtappdaemon-controller add controller %s", controllerKind.String()) + klog.Infof("yurtappdaemon-controller add controller %s", controllerResource.String()) return add(mgr, newReconciler(mgr)) } diff --git a/pkg/controller/yurtappset/yurtappset_controller.go b/pkg/controller/yurtappset/yurtappset_controller.go index 572403328f1..eeeb4c79e49 100644 --- a/pkg/controller/yurtappset/yurtappset_controller.go +++ b/pkg/controller/yurtappset/yurtappset_controller.go @@ -43,7 +43,6 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/controller/yurtappset/adapter" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" ) func init() { @@ -52,7 +51,7 @@ func init() { var ( concurrentReconciles = 3 - controllerKind = unitv1alpha1.SchemeGroupVersion.WithKind("YurtAppSet") + controllerResource = unitv1alpha1.SchemeGroupVersion.WithResource("yurtappsets") ) const ( @@ -75,19 +74,17 @@ func Format(format string, args ...interface{}) string { // Add creates a new YurtAppSet Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(c *config.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - klog.Errorf(Format("DiscoverGVK error")) - return nil + if _, err := mgr.GetRESTMapper().KindFor(controllerResource); err != nil { + klog.Infof("resource %s doesn't exist", controllerResource.String()) + return err } - klog.Infof("yurtappset-controller add controller %s", controllerKind.String()) + klog.Infof("yurtappset-controller add controller %s", controllerResource.String()) return add(mgr, newReconciler(c, mgr)) } // newReconciler returns a new reconcile.Reconciler func newReconciler(c *config.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { - klog.Infof("yurtappset-controller newReconciler %s", controllerKind.String()) - return &ReconcileYurtAppSet{ Client: mgr.GetClient(), scheme: mgr.GetScheme(), diff --git a/pkg/controller/yurtstaticset/yurtstaticset_controller.go b/pkg/controller/yurtstaticset/yurtstaticset_controller.go index c3469a05645..2047b64a051 100644 --- a/pkg/controller/yurtstaticset/yurtstaticset_controller.go +++ b/pkg/controller/yurtstaticset/yurtstaticset_controller.go @@ -45,8 +45,6 @@ import ( "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/config" "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/upgradeinfo" "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" - utilclient "github.com/openyurtio/openyurt/pkg/util/client" - utildiscovery "github.com/openyurtio/openyurt/pkg/util/discovery" ) func init() { @@ -55,7 +53,7 @@ func init() { var ( concurrentReconciles = 3 - controllerKind = appsv1alpha1.SchemeGroupVersion.WithKind("YurtStaticSet") + controllerResource = appsv1alpha1.SchemeGroupVersion.WithResource("yurtstaticsets") True = true ) @@ -128,11 +126,12 @@ func Format(format string, args ...interface{}) string { // Add creates a new YurtStaticSet Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { - if !utildiscovery.DiscoverGVK(controllerKind) { - return nil + if _, err := mgr.GetRESTMapper().KindFor(controllerResource); err != nil { + klog.Infof("resource %s doesn't exist", controllerResource.String()) + return err } - klog.Infof("yurtstaticset-controller add controller %s", controllerKind.String()) + klog.Infof("yurtstaticset-controller add controller %s", controllerResource.String()) return add(mgr, newReconciler(c, mgr)) } @@ -149,7 +148,7 @@ type ReconcileYurtStaticSet struct { // newReconciler returns a new reconcile.Reconciler func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { return &ReconcileYurtStaticSet{ - Client: utilclient.NewClientFromManager(mgr, ControllerName), + Client: mgr.GetClient(), scheme: mgr.GetScheme(), recorder: mgr.GetEventRecorderFor(ControllerName), Configuration: c.ComponentConfig.YurtStaticSetController, diff --git a/pkg/profile/profile_test.go b/pkg/profile/profile_test.go index cfb7f2e7e78..998b6f91a75 100644 --- a/pkg/profile/profile_test.go +++ b/pkg/profile/profile_test.go @@ -18,28 +18,53 @@ package profile import ( "net/http" + "net/http/httptest" "testing" "github.com/gorilla/mux" ) -func fakeServer(h http.Handler) error { - err := http.ListenAndServe(":9090", h) - return err -} - +// TestInstall checks Install function correctly sets up routes func TestInstall(t *testing.T) { - t.Run("TestInstall", func(t *testing.T) { - m := mux.NewRouter() - Install(m) - go fakeServer(m) - r, err := http.Get("http://localhost:9090/debug/pprof/") + router := mux.NewRouter() + Install(router) + + // Define the routes we expect to exist and their expected handlers + expectedRoutes := map[string]string{ + "/debug/pprof/profile": "profile", + "/debug/pprof/symbol": "symbol", + "/debug/pprof/trace": "trace", + } + + // For each route, make a request and check the handler that's called + for route := range expectedRoutes { + req, err := http.NewRequest("GET", route, nil) if err != nil { - t.Errorf("failed to send request to fake server, %v", err) + t.Fatal(err) } + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) - if r.StatusCode != http.StatusOK { - t.Error(err) + if rr.Code != http.StatusOK { + t.Errorf("handler(%s) returned wrong status code: got %v want %v", route, rr.Code, http.StatusOK) } - }) + } +} + +// TestRedirectTo checks redirect to the desired location +func TestRedirectTo(t *testing.T) { + destination := "/destination" + redirect := redirectTo(destination) + + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + + redirect(rr, req) + + if location := rr.Header().Get("Location"); location != destination { + t.Errorf("expected redirect to %s, got %s", destination, location) + } } diff --git a/pkg/util/client/client.go b/pkg/util/client/client.go deleted file mode 100644 index 46f8e9b181f..00000000000 --- a/pkg/util/client/client.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package client - -import ( - "fmt" - - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/cluster" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -func NewClientFromManager(mgr manager.Manager, name string) client.Client { - cfg := rest.CopyConfig(mgr.GetConfig()) - cfg.UserAgent = fmt.Sprintf("yurt-manager/%s", name) - - delegatingClient, _ := cluster.DefaultNewClient(mgr.GetCache(), cfg, client.Options{Scheme: mgr.GetScheme(), Mapper: mgr.GetRESTMapper()}) - return delegatingClient -} diff --git a/pkg/util/discovery/discovery.go b/pkg/util/discovery/discovery.go deleted file mode 100644 index de75f164e26..00000000000 --- a/pkg/util/discovery/discovery.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package discovery - -import ( - "fmt" - "time" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/util/retry" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - - "github.com/openyurtio/openyurt/pkg/apis" - "github.com/openyurtio/openyurt/pkg/client" -) - -var ( - internalScheme = runtime.NewScheme() - - errKindNotFound = fmt.Errorf("kind not found in group version resources") - backOff = wait.Backoff{ - Steps: 4, - Duration: 500 * time.Millisecond, - Factor: 5.0, - Jitter: 0.1, - } -) - -func init() { - _ = apis.AddToScheme(internalScheme) -} - -func DiscoverGVK(gvk schema.GroupVersionKind) bool { - genericClient := client.GetGenericClient() - if genericClient == nil { - return true - } - discoveryClient := genericClient.DiscoveryClient - - startTime := time.Now() - err := retry.OnError(backOff, func(err error) bool { return true }, func() error { - resourceList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) - if err != nil { - return err - } - for _, r := range resourceList.APIResources { - if r.Kind == gvk.Kind { - return nil - } - } - return errKindNotFound - }) - - if err != nil { - if err == errKindNotFound { - klog.Warningf("Not found kind %s in group version %s, waiting time %s", gvk.Kind, gvk.GroupVersion().String(), time.Since(startTime)) - return false - } - - // This might be caused by abnormal apiserver or etcd, ignore it - klog.Errorf("Failed to find resources in group version %s: %v, waiting time %s", gvk.GroupVersion().String(), err, time.Since(startTime)) - } - - return true -} - -func DiscoverObject(obj runtime.Object) bool { - gvk, err := apiutil.GVKForObject(obj, internalScheme) - if err != nil { - klog.Warningf("Not recognized object %T in scheme: %v", obj, err) - return false - } - return DiscoverGVK(gvk) -} diff --git a/pkg/webhook/util/controller/webhook_controller.go b/pkg/webhook/util/controller/webhook_controller.go index 5ca3e807bbf..2734dad20e9 100644 --- a/pkg/webhook/util/controller/webhook_controller.go +++ b/pkg/webhook/util/controller/webhook_controller.go @@ -43,7 +43,6 @@ import ( "k8s.io/klog/v2" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - extclient "github.com/openyurtio/openyurt/pkg/client" webhookutil "github.com/openyurtio/openyurt/pkg/webhook/util" "github.com/openyurtio/openyurt/pkg/webhook/util/configuration" "github.com/openyurtio/openyurt/pkg/webhook/util/generator" @@ -80,8 +79,12 @@ type Controller struct { } func New(handlers map[string]struct{}, cc *config.CompletedConfig, restCfg *rest.Config) (*Controller, error) { + kubeClient, err := clientset.NewForConfig(restCfg) + if err != nil { + return nil, err + } c := &Controller{ - kubeClient: extclient.GetGenericClientWithName("webhook-controller").KubeClient, + kubeClient: kubeClient, handlers: handlers, queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "webhook-controller"), webhookPort: cc.ComponentConfig.Generic.WebhookPort, From de07563bea443632f7723672122e6eb04bc17269 Mon Sep 17 00:00:00 2001 From: rambohe Date: Thu, 17 Aug 2023 14:54:24 +0800 Subject: [PATCH 78/93] move controller and webhook into pkg/yurtmanager (#1661) Signed-off-by: rambohe-ch --- cmd/yurt-manager/app/config/config.go | 2 +- cmd/yurt-manager/app/manager.go | 8 ++--- .../app/options/gatewaycontroller.go | 2 +- cmd/yurt-manager/app/options/generic.go | 2 +- .../app/options/nodepoolcontroller.go | 2 +- .../app/options/platformadmincontroller.go | 2 +- .../app/options/yurtappdaemoncontroller.go | 2 +- .../app/options/yurtappsetcontroller.go | 2 +- .../app/options/yurtstaticsetcontroller.go | 2 +- pkg/{ => util}/profile/profile.go | 0 pkg/{ => util}/profile/profile_test.go | 0 pkg/yurtadm/cmd/join/join.go | 2 +- pkg/yurtadm/cmd/staticpods/install/install.go | 2 +- .../cmd/staticpods/uninstall/uninstall.go | 2 +- pkg/yurthub/otaupdate/ota.go | 2 +- pkg/yurthub/otaupdate/upgrader/static_pod.go | 2 +- .../otaupdate/upgrader/static_pod_test.go | 2 +- pkg/yurthub/otaupdate/util/util.go | 2 +- pkg/yurthub/server/server.go | 2 +- .../controller/apis/config/types.go | 12 +++---- .../controller/controller.go | 32 ++++++++--------- .../csrapprover/csrapprover_controller.go | 2 +- .../csrapprover_controller_test.go | 0 .../daemon_pod_updater_controller.go | 2 +- .../daemon_pod_updater_controller_test.go | 2 +- .../kubernetes/controller_utils.go | 0 .../kubernetes/controller_utils_test.go | 0 .../daemonpodupdater/kubernetes/pod_util.go | 0 .../kubernetes/pod_util_test.go | 0 .../controller/daemonpodupdater/util.go | 8 ++--- .../controller/daemonpodupdater/util_test.go | 0 .../controller/nodepool/config/types.go | 0 .../nodepool/nodepool_controller.go | 2 +- .../nodepool/nodepool_controller_test.go | 2 +- .../nodepool/nodepool_enqueue_handlers.go | 0 .../nodepool_enqueue_handlers_test.go | 0 .../controller/nodepool/util.go | 2 +- .../controller/nodepool/util_test.go | 0 .../config/EdgeXConfig/config-nosecty.json | 0 .../config/EdgeXConfig/config.json | 0 .../config/EdgeXConfig/manifest.yaml | 0 .../controller/platformadmin/config/types.go | 0 .../platformadmin/platformadmin_controller.go | 4 +-- .../controller/platformadmin/util.go | 4 +-- .../platformadmin/utils/conditions.go | 0 .../platformadmin/utils/fieldindexer.go | 0 .../controller/platformadmin/utils/version.go | 0 .../controller/raven/common.go | 0 .../raven/gatewaypickup/config/types.go | 0 .../gateway_pickup_controller.go | 8 ++--- .../gateway_pickup_controller_test.go | 4 +-- .../gateway_pickup_enqueue_handlers.go | 2 +- .../controller/raven/utils/utils.go | 0 .../servicetopology/adapter/adapter.go | 0 .../adapter/endpoints_adapter.go | 0 .../adapter/endpoints_adapter_test.go | 0 .../adapter/endpointslicev1_adapter.go | 0 .../adapter/endpointslicev1_adapter_test.go | 0 .../adapter/endpointslicev1beta1_adapter.go | 0 .../endpointslicev1beta1_adapter_test.go | 0 .../controller/servicetopology/common.go | 0 .../endpoints/endpoints_controller.go | 4 +-- .../endpoints/endpoints_enqueue_handlers.go | 4 +-- .../endpointslice/endpointslice_controller.go | 4 +-- .../endpointslice_enqueue_handlers.go | 4 +-- .../controller/servicetopology/util/util.go | 0 .../controller/util/controller_utils.go | 0 .../controller/util/controller_utils_test.go | 0 .../controller/util/node/controller_utils.go | 0 .../controller/util/refmanager/ref_manager.go | 0 .../util/refmanager/ref_manager_test.go | 0 .../controller/util/tools.go | 0 .../controller/util/tools_test.go | 0 .../controller/yurtappdaemon/config/types.go | 0 .../nodepool_enqueue_handlers.go | 0 .../nodepool_enqueue_handlers_test.go | 0 .../controller/yurtappdaemon/revision.go | 2 +- .../controller/yurtappdaemon/revision_test.go | 0 .../controller/yurtappdaemon/util.go | 0 .../controller/yurtappdaemon/util_test.go | 0 .../workloadcontroller/controller.go | 0 .../deployment_controller.go | 2 +- .../deployment_controller_test.go | 0 .../statefulset_controller.go | 0 .../yurtappdaemon/workloadcontroller/util.go | 0 .../workloadcontroller/util_test.go | 0 .../workloadcontroller/workload.go | 0 .../workloadcontroller/workload_test.go | 0 .../yurtappdaemon/yurtappdaemon_controller.go | 4 +-- .../yurtappdaemon_controller_test.go | 2 +- .../controller/yurtappset/adapter/adapter.go | 0 .../yurtappset/adapter/adapter_util.go | 0 .../yurtappset/adapter/adapter_util_test.go | 0 .../yurtappset/adapter/deployment_adapter.go | 0 .../adapter/deployment_adapter_test.go | 0 .../yurtappset/adapter/statefulset_adapter.go | 0 .../adapter/statefulset_adapter_test.go | 0 .../controller/yurtappset/config/types.go | 0 .../controller/yurtappset/pool.go | 2 +- .../controller/yurtappset/pool_control.go | 4 +-- .../yurtappset/pool_controller_test.go | 2 +- .../controller/yurtappset/revision.go | 2 +- .../controller/yurtappset/revision_test.go | 0 .../yurtappset/yurtappset_controller.go | 2 +- .../yurtappset_controller_statefulset_test.go | 0 .../yurtappset_controller_suite_test.go | 0 .../yurtappset/yurtappset_controller_test.go | 6 ++-- .../yurtappset/yurtappset_controller_utils.go | 0 .../yurtappset/yurtappset_update.go | 2 +- .../yurtcoordinator/cert/certificate.go | 0 .../yurtcoordinator/cert/certificate_test.go | 0 .../controller/yurtcoordinator/cert/secret.go | 0 .../yurtcoordinator/cert/secret_test.go | 0 .../controller/yurtcoordinator/cert/util.go | 0 .../yurtcoordinator/cert/util_test.go | 0 .../cert/yurtcoordinatorcert_controller.go | 0 .../yurtcoordinatorcert_controller_test.go | 0 .../yurtcoordinator/constant/constant.go | 0 .../delegatelease/delegatelease_controller.go | 6 ++-- .../delegatelease_controller_test.go | 2 +- .../podbinding/podbinding_controller.go | 2 +- .../podbinding/podbinding_controller_test.go | 0 .../controller/yurtcoordinator/utils/lease.go | 2 +- .../yurtcoordinator/utils/lease_test.go | 0 .../yurtcoordinator/utils/taints.go | 0 .../yurtcoordinator/utils/taints_test.go | 2 +- .../controller/yurtstaticset/config/types.go | 0 .../yurtstaticset/upgradeinfo/upgrade_info.go | 2 +- .../upgradeinfo/upgrade_info_test.go | 0 .../controller/yurtstaticset/util/util.go | 0 .../yurtstaticset/yurtstaticset_controller.go | 6 ++-- .../yurtstaticset_controller_test.go | 2 +- .../webhook/builder/defaulter_custom.go | 0 .../webhook/builder/validator_custom.go | 0 .../webhook/builder/webhook.go | 2 +- .../gateway/v1alpha1/gateway_default.go | 0 .../gateway/v1alpha1/gateway_handler.go | 2 +- .../gateway/v1alpha1/gateway_validation.go | 0 .../gateway/v1beta1/gateway_default.go | 0 .../gateway/v1beta1/gateway_handler.go | 2 +- .../gateway/v1beta1/gateway_validation.go | 0 .../webhook/node/v1/node_default.go | 0 .../webhook/node/v1/node_default_test.go | 0 .../webhook/node/v1/node_handler.go | 4 +-- .../webhook/node/v1/node_validation.go | 0 .../webhook/node/v1/node_validation_test.go | 0 .../nodepool/v1beta1/nodepool_default.go | 0 .../nodepool/v1beta1/nodepool_default_test.go | 0 .../nodepool/v1beta1/nodepool_handler.go | 2 +- .../nodepool/v1beta1/nodepool_validation.go | 5 +++ .../v1beta1/nodepool_validation_test.go | 0 .../v1alpha1/platformadmin_default.go | 0 .../v1alpha1/platformadmin_handler.go | 4 +-- .../v1alpha1/platformadmin_validation.go | 2 +- .../v1alpha2/platformadmin_default.go | 0 .../v1alpha2/platformadmin_handler.go | 4 +-- .../v1alpha2/platformadmin_validation.go | 2 +- .../webhook/pod/v1/pod_handler.go | 4 +-- .../webhook/pod/v1/pod_validation.go | 2 +- pkg/{ => yurtmanager}/webhook/server.go | 36 +++++++++---------- .../util/configuration/configuration.go | 2 +- .../util/controller/webhook_controller.go | 8 ++--- .../webhook/util/generator/certgenerator.go | 0 .../webhook/util/generator/selfsigned.go | 0 .../webhook/util/generator/util.go | 0 pkg/{ => yurtmanager}/webhook/util/util.go | 0 .../util/writer/atomic/atomic_writer.go | 0 .../webhook/util/writer/certwriter.go | 2 +- .../webhook/util/writer/error.go | 0 .../webhook/util/writer/fs.go | 4 +-- .../webhook/util/writer/secret.go | 2 +- .../v1alpha1/yurtappdaemon_default.go | 0 .../v1alpha1/yurtappdaemon_handler.go | 2 +- .../v1alpha1/yurtappdaemon_validation.go | 0 .../webhook/yurtappset/v1alpha1/validate.go | 0 .../yurtappset/v1alpha1/yurtappset_default.go | 0 .../yurtappset/v1alpha1/yurtappset_handler.go | 2 +- .../v1alpha1/yurtappset_validation.go | 0 .../v1alpha1/yurtappset_webhook_test.go | 0 .../v1alpha1/yurtstaticset_default.go | 0 .../v1alpha1/yurtstaticset_handler.go | 2 +- .../v1alpha1/yurtstaticset_validation.go | 0 pkg/yurttunnel/util/util.go | 2 +- 183 files changed, 149 insertions(+), 144 deletions(-) rename pkg/{ => util}/profile/profile.go (100%) rename pkg/{ => util}/profile/profile_test.go (100%) rename pkg/{ => yurtmanager}/controller/apis/config/types.go (81%) rename pkg/{ => yurtmanager}/controller/controller.go (71%) rename pkg/{ => yurtmanager}/controller/csrapprover/csrapprover_controller.go (99%) rename pkg/{ => yurtmanager}/controller/csrapprover/csrapprover_controller_test.go (100%) rename pkg/{ => yurtmanager}/controller/daemonpodupdater/daemon_pod_updater_controller.go (99%) rename pkg/{ => yurtmanager}/controller/daemonpodupdater/daemon_pod_updater_controller_test.go (99%) rename pkg/{ => yurtmanager}/controller/daemonpodupdater/kubernetes/controller_utils.go (100%) rename pkg/{ => yurtmanager}/controller/daemonpodupdater/kubernetes/controller_utils_test.go (100%) rename pkg/{ => yurtmanager}/controller/daemonpodupdater/kubernetes/pod_util.go (100%) rename pkg/{ => yurtmanager}/controller/daemonpodupdater/kubernetes/pod_util_test.go (100%) rename pkg/{ => yurtmanager}/controller/daemonpodupdater/util.go (96%) rename pkg/{ => yurtmanager}/controller/daemonpodupdater/util_test.go (100%) rename pkg/{ => yurtmanager}/controller/nodepool/config/types.go (100%) rename pkg/{ => yurtmanager}/controller/nodepool/nodepool_controller.go (98%) rename pkg/{ => yurtmanager}/controller/nodepool/nodepool_controller_test.go (98%) rename pkg/{ => yurtmanager}/controller/nodepool/nodepool_enqueue_handlers.go (100%) rename pkg/{ => yurtmanager}/controller/nodepool/nodepool_enqueue_handlers_test.go (100%) rename pkg/{ => yurtmanager}/controller/nodepool/util.go (98%) rename pkg/{ => yurtmanager}/controller/nodepool/util_test.go (100%) rename pkg/{ => yurtmanager}/controller/platformadmin/config/EdgeXConfig/config-nosecty.json (100%) rename pkg/{ => yurtmanager}/controller/platformadmin/config/EdgeXConfig/config.json (100%) rename pkg/{ => yurtmanager}/controller/platformadmin/config/EdgeXConfig/manifest.yaml (100%) rename pkg/{ => yurtmanager}/controller/platformadmin/config/types.go (100%) rename pkg/{ => yurtmanager}/controller/platformadmin/platformadmin_controller.go (99%) rename pkg/{ => yurtmanager}/controller/platformadmin/util.go (95%) rename pkg/{ => yurtmanager}/controller/platformadmin/utils/conditions.go (100%) rename pkg/{ => yurtmanager}/controller/platformadmin/utils/fieldindexer.go (100%) rename pkg/{ => yurtmanager}/controller/platformadmin/utils/version.go (100%) rename pkg/{ => yurtmanager}/controller/raven/common.go (100%) rename pkg/{ => yurtmanager}/controller/raven/gatewaypickup/config/types.go (100%) rename pkg/{ => yurtmanager}/controller/raven/gatewaypickup/gateway_pickup_controller.go (97%) rename pkg/{ => yurtmanager}/controller/raven/gatewaypickup/gateway_pickup_controller_test.go (98%) rename pkg/{ => yurtmanager}/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go (98%) rename pkg/{ => yurtmanager}/controller/raven/utils/utils.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/adapter/adapter.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/adapter/endpoints_adapter.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/adapter/endpoints_adapter_test.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/adapter/endpointslicev1_adapter.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/adapter/endpointslicev1_adapter_test.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/common.go (100%) rename pkg/{ => yurtmanager}/controller/servicetopology/endpoints/endpoints_controller.go (96%) rename pkg/{ => yurtmanager}/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go (93%) rename pkg/{ => yurtmanager}/controller/servicetopology/endpointslice/endpointslice_controller.go (96%) rename pkg/{ => yurtmanager}/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go (93%) rename pkg/{ => yurtmanager}/controller/servicetopology/util/util.go (100%) rename pkg/{ => yurtmanager}/controller/util/controller_utils.go (100%) rename pkg/{ => yurtmanager}/controller/util/controller_utils_test.go (100%) rename pkg/{ => yurtmanager}/controller/util/node/controller_utils.go (100%) rename pkg/{ => yurtmanager}/controller/util/refmanager/ref_manager.go (100%) rename pkg/{ => yurtmanager}/controller/util/refmanager/ref_manager_test.go (100%) rename pkg/{ => yurtmanager}/controller/util/tools.go (100%) rename pkg/{ => yurtmanager}/controller/util/tools_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/config/types.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/nodepool_enqueue_handlers.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/revision.go (99%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/revision_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/util.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/util_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/workloadcontroller/controller.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/workloadcontroller/deployment_controller.go (98%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/workloadcontroller/statefulset_controller.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/workloadcontroller/util.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/workloadcontroller/util_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/workloadcontroller/workload.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/workloadcontroller/workload_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/yurtappdaemon_controller.go (99%) rename pkg/{ => yurtmanager}/controller/yurtappdaemon/yurtappdaemon_controller_test.go (99%) rename pkg/{ => yurtmanager}/controller/yurtappset/adapter/adapter.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/adapter/adapter_util.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/adapter/adapter_util_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/adapter/deployment_adapter.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/adapter/deployment_adapter_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/adapter/statefulset_adapter.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/adapter/statefulset_adapter_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/config/types.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/pool.go (96%) rename pkg/{ => yurtmanager}/controller/yurtappset/pool_control.go (97%) rename pkg/{ => yurtmanager}/controller/yurtappset/pool_controller_test.go (98%) rename pkg/{ => yurtmanager}/controller/yurtappset/revision.go (99%) rename pkg/{ => yurtmanager}/controller/yurtappset/revision_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/yurtappset_controller.go (99%) rename pkg/{ => yurtmanager}/controller/yurtappset/yurtappset_controller_statefulset_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/yurtappset_controller_suite_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/yurtappset_controller_test.go (97%) rename pkg/{ => yurtmanager}/controller/yurtappset/yurtappset_controller_utils.go (100%) rename pkg/{ => yurtmanager}/controller/yurtappset/yurtappset_update.go (99%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/cert/certificate.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/cert/certificate_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/cert/secret.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/cert/secret_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/cert/util.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/cert/util_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/constant/constant.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/delegatelease/delegatelease_controller.go (96%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go (93%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/podbinding/podbinding_controller.go (98%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/podbinding/podbinding_controller_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/utils/lease.go (94%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/utils/lease_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/utils/taints.go (100%) rename pkg/{ => yurtmanager}/controller/yurtcoordinator/utils/taints_test.go (93%) rename pkg/{ => yurtmanager}/controller/yurtstaticset/config/types.go (100%) rename pkg/{ => yurtmanager}/controller/yurtstaticset/upgradeinfo/upgrade_info.go (99%) rename pkg/{ => yurtmanager}/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go (100%) rename pkg/{ => yurtmanager}/controller/yurtstaticset/util/util.go (100%) rename pkg/{ => yurtmanager}/controller/yurtstaticset/yurtstaticset_controller.go (98%) rename pkg/{ => yurtmanager}/controller/yurtstaticset/yurtstaticset_controller_test.go (98%) rename pkg/{ => yurtmanager}/webhook/builder/defaulter_custom.go (100%) rename pkg/{ => yurtmanager}/webhook/builder/validator_custom.go (100%) rename pkg/{ => yurtmanager}/webhook/builder/webhook.go (98%) rename pkg/{ => yurtmanager}/webhook/gateway/v1alpha1/gateway_default.go (100%) rename pkg/{ => yurtmanager}/webhook/gateway/v1alpha1/gateway_handler.go (96%) rename pkg/{ => yurtmanager}/webhook/gateway/v1alpha1/gateway_validation.go (100%) rename pkg/{ => yurtmanager}/webhook/gateway/v1beta1/gateway_default.go (100%) rename pkg/{ => yurtmanager}/webhook/gateway/v1beta1/gateway_handler.go (97%) rename pkg/{ => yurtmanager}/webhook/gateway/v1beta1/gateway_validation.go (100%) rename pkg/{ => yurtmanager}/webhook/node/v1/node_default.go (100%) rename pkg/{ => yurtmanager}/webhook/node/v1/node_default_test.go (100%) rename pkg/{ => yurtmanager}/webhook/node/v1/node_handler.go (93%) rename pkg/{ => yurtmanager}/webhook/node/v1/node_validation.go (100%) rename pkg/{ => yurtmanager}/webhook/node/v1/node_validation_test.go (100%) rename pkg/{ => yurtmanager}/webhook/nodepool/v1beta1/nodepool_default.go (100%) rename pkg/{ => yurtmanager}/webhook/nodepool/v1beta1/nodepool_default_test.go (100%) rename pkg/{ => yurtmanager}/webhook/nodepool/v1beta1/nodepool_handler.go (97%) rename pkg/{ => yurtmanager}/webhook/nodepool/v1beta1/nodepool_validation.go (95%) rename pkg/{ => yurtmanager}/webhook/nodepool/v1beta1/nodepool_validation_test.go (100%) rename pkg/{ => yurtmanager}/webhook/platformadmin/v1alpha1/platformadmin_default.go (100%) rename pkg/{ => yurtmanager}/webhook/platformadmin/v1alpha1/platformadmin_handler.go (94%) rename pkg/{ => yurtmanager}/webhook/platformadmin/v1alpha1/platformadmin_validation.go (98%) rename pkg/{ => yurtmanager}/webhook/platformadmin/v1alpha2/platformadmin_default.go (100%) rename pkg/{ => yurtmanager}/webhook/platformadmin/v1alpha2/platformadmin_handler.go (95%) rename pkg/{ => yurtmanager}/webhook/platformadmin/v1alpha2/platformadmin_validation.go (98%) rename pkg/{ => yurtmanager}/webhook/pod/v1/pod_handler.go (92%) rename pkg/{ => yurtmanager}/webhook/pod/v1/pod_validation.go (98%) rename pkg/{ => yurtmanager}/webhook/server.go (76%) rename pkg/{ => yurtmanager}/webhook/util/configuration/configuration.go (98%) rename pkg/{ => yurtmanager}/webhook/util/controller/webhook_controller.go (97%) rename pkg/{ => yurtmanager}/webhook/util/generator/certgenerator.go (100%) rename pkg/{ => yurtmanager}/webhook/util/generator/selfsigned.go (100%) rename pkg/{ => yurtmanager}/webhook/util/generator/util.go (100%) rename pkg/{ => yurtmanager}/webhook/util/util.go (100%) rename pkg/{ => yurtmanager}/webhook/util/writer/atomic/atomic_writer.go (100%) rename pkg/{ => yurtmanager}/webhook/util/writer/certwriter.go (97%) rename pkg/{ => yurtmanager}/webhook/util/writer/error.go (100%) rename pkg/{ => yurtmanager}/webhook/util/writer/fs.go (97%) rename pkg/{ => yurtmanager}/webhook/util/writer/secret.go (98%) rename pkg/{ => yurtmanager}/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_default.go (100%) rename pkg/{ => yurtmanager}/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_handler.go (97%) rename pkg/{ => yurtmanager}/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go (100%) rename pkg/{ => yurtmanager}/webhook/yurtappset/v1alpha1/validate.go (100%) rename pkg/{ => yurtmanager}/webhook/yurtappset/v1alpha1/yurtappset_default.go (100%) rename pkg/{ => yurtmanager}/webhook/yurtappset/v1alpha1/yurtappset_handler.go (97%) rename pkg/{ => yurtmanager}/webhook/yurtappset/v1alpha1/yurtappset_validation.go (100%) rename pkg/{ => yurtmanager}/webhook/yurtappset/v1alpha1/yurtappset_webhook_test.go (100%) rename pkg/{ => yurtmanager}/webhook/yurtstaticset/v1alpha1/yurtstaticset_default.go (100%) rename pkg/{ => yurtmanager}/webhook/yurtstaticset/v1alpha1/yurtstaticset_handler.go (97%) rename pkg/{ => yurtmanager}/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go (100%) diff --git a/cmd/yurt-manager/app/config/config.go b/cmd/yurt-manager/app/config/config.go index ed471d0c08d..75f9424f50a 100644 --- a/cmd/yurt-manager/app/config/config.go +++ b/cmd/yurt-manager/app/config/config.go @@ -17,7 +17,7 @@ limitations under the License. package config import ( - yurtctrlmgrconfig "github.com/openyurtio/openyurt/pkg/controller/apis/config" + yurtctrlmgrconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/apis/config" ) // Config is the main context object for the controller manager. diff --git a/cmd/yurt-manager/app/manager.go b/cmd/yurt-manager/app/manager.go index f615dfd103a..05e2a2c40bd 100644 --- a/cmd/yurt-manager/app/manager.go +++ b/cmd/yurt-manager/app/manager.go @@ -37,11 +37,11 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/options" "github.com/openyurtio/openyurt/pkg/apis" - "github.com/openyurtio/openyurt/pkg/controller" - "github.com/openyurtio/openyurt/pkg/profile" "github.com/openyurtio/openyurt/pkg/projectinfo" - "github.com/openyurtio/openyurt/pkg/webhook" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/util/profile" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) var ( diff --git a/cmd/yurt-manager/app/options/gatewaycontroller.go b/cmd/yurt-manager/app/options/gatewaycontroller.go index e125dbbff1f..344f1874e69 100644 --- a/cmd/yurt-manager/app/options/gatewaycontroller.go +++ b/cmd/yurt-manager/app/options/gatewaycontroller.go @@ -19,7 +19,7 @@ package options import ( "github.com/spf13/pflag" - "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup/config" ) type GatewayPickupControllerOptions struct { diff --git a/cmd/yurt-manager/app/options/generic.go b/cmd/yurt-manager/app/options/generic.go index 247e7318d6d..919fd504d04 100644 --- a/cmd/yurt-manager/app/options/generic.go +++ b/cmd/yurt-manager/app/options/generic.go @@ -21,8 +21,8 @@ import ( "github.com/spf13/pflag" - "github.com/openyurtio/openyurt/pkg/controller/apis/config" "github.com/openyurtio/openyurt/pkg/features" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/apis/config" ) const enableAll = "*" diff --git a/cmd/yurt-manager/app/options/nodepoolcontroller.go b/cmd/yurt-manager/app/options/nodepoolcontroller.go index 8760760ef17..114a2b7c5e9 100644 --- a/cmd/yurt-manager/app/options/nodepoolcontroller.go +++ b/cmd/yurt-manager/app/options/nodepoolcontroller.go @@ -19,7 +19,7 @@ package options import ( "github.com/spf13/pflag" - "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool/config" ) type NodePoolControllerOptions struct { diff --git a/cmd/yurt-manager/app/options/platformadmincontroller.go b/cmd/yurt-manager/app/options/platformadmincontroller.go index ce8aa99e682..969853905a6 100644 --- a/cmd/yurt-manager/app/options/platformadmincontroller.go +++ b/cmd/yurt-manager/app/options/platformadmincontroller.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/pflag" - "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" ) type PlatformAdminControllerOptions struct { diff --git a/cmd/yurt-manager/app/options/yurtappdaemoncontroller.go b/cmd/yurt-manager/app/options/yurtappdaemoncontroller.go index 70c88313076..de105bfa8d6 100644 --- a/cmd/yurt-manager/app/options/yurtappdaemoncontroller.go +++ b/cmd/yurt-manager/app/options/yurtappdaemoncontroller.go @@ -19,7 +19,7 @@ package options import ( "github.com/spf13/pflag" - "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon/config" ) type YurtAppDaemonControllerOptions struct { diff --git a/cmd/yurt-manager/app/options/yurtappsetcontroller.go b/cmd/yurt-manager/app/options/yurtappsetcontroller.go index 7ddca638cba..6f0d537b8e2 100644 --- a/cmd/yurt-manager/app/options/yurtappsetcontroller.go +++ b/cmd/yurt-manager/app/options/yurtappsetcontroller.go @@ -19,7 +19,7 @@ package options import ( "github.com/spf13/pflag" - "github.com/openyurtio/openyurt/pkg/controller/yurtappset/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/config" ) type YurtAppSetControllerOptions struct { diff --git a/cmd/yurt-manager/app/options/yurtstaticsetcontroller.go b/cmd/yurt-manager/app/options/yurtstaticsetcontroller.go index e26da86236c..fcb27e236d8 100644 --- a/cmd/yurt-manager/app/options/yurtstaticsetcontroller.go +++ b/cmd/yurt-manager/app/options/yurtstaticsetcontroller.go @@ -19,7 +19,7 @@ package options import ( "github.com/spf13/pflag" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/config" ) const DefaultUpgradeWorkerImage = "openyurt/node-servant:latest" diff --git a/pkg/profile/profile.go b/pkg/util/profile/profile.go similarity index 100% rename from pkg/profile/profile.go rename to pkg/util/profile/profile.go diff --git a/pkg/profile/profile_test.go b/pkg/util/profile/profile_test.go similarity index 100% rename from pkg/profile/profile_test.go rename to pkg/util/profile/profile_test.go diff --git a/pkg/yurtadm/cmd/join/join.go b/pkg/yurtadm/cmd/join/join.go index 1406c75b88d..9e554abb945 100644 --- a/pkg/yurtadm/cmd/join/join.go +++ b/pkg/yurtadm/cmd/join/join.go @@ -31,7 +31,6 @@ import ( "k8s.io/klog/v2" "github.com/openyurtio/openyurt/pkg/apis/apps" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" kubeconfigutil "github.com/openyurtio/openyurt/pkg/util/kubeconfig" "github.com/openyurtio/openyurt/pkg/util/kubernetes/kubeadm/app/util/apiclient" "github.com/openyurtio/openyurt/pkg/yurtadm/cmd/join/joindata" @@ -40,6 +39,7 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" "github.com/openyurtio/openyurt/pkg/yurtadm/util/yurthub" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/util" ) type joinOptions struct { diff --git a/pkg/yurtadm/cmd/staticpods/install/install.go b/pkg/yurtadm/cmd/staticpods/install/install.go index a58606dcd59..c3fc38e38fa 100644 --- a/pkg/yurtadm/cmd/staticpods/install/install.go +++ b/pkg/yurtadm/cmd/staticpods/install/install.go @@ -25,11 +25,11 @@ import ( flag "github.com/spf13/pflag" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" "github.com/openyurtio/openyurt/pkg/yurtadm/constants" "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" "github.com/openyurtio/openyurt/pkg/yurtadm/util/yurthub" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/util" ) type installOptions struct { diff --git a/pkg/yurtadm/cmd/staticpods/uninstall/uninstall.go b/pkg/yurtadm/cmd/staticpods/uninstall/uninstall.go index 97a142b2c35..6628e2f3eda 100644 --- a/pkg/yurtadm/cmd/staticpods/uninstall/uninstall.go +++ b/pkg/yurtadm/cmd/staticpods/uninstall/uninstall.go @@ -25,11 +25,11 @@ import ( flag "github.com/spf13/pflag" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" "github.com/openyurtio/openyurt/pkg/yurtadm/constants" "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" yurtadmutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/kubernetes" "github.com/openyurtio/openyurt/pkg/yurtadm/util/yurthub" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/util" ) type uninstallOptions struct { diff --git a/pkg/yurthub/otaupdate/ota.go b/pkg/yurthub/otaupdate/ota.go index 4b44e10d8b2..48cdcacfc9e 100644 --- a/pkg/yurthub/otaupdate/ota.go +++ b/pkg/yurthub/otaupdate/ota.go @@ -28,12 +28,12 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater" "github.com/openyurtio/openyurt/pkg/yurthub/cachemanager" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/rest" upgrade "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate/upgrader" "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate/util" "github.com/openyurtio/openyurt/pkg/yurthub/storage" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/daemonpodupdater" ) const ( diff --git a/pkg/yurthub/otaupdate/upgrader/static_pod.go b/pkg/yurthub/otaupdate/upgrader/static_pod.go index bc4c3697cfc..1476976e0de 100644 --- a/pkg/yurthub/otaupdate/upgrader/static_pod.go +++ b/pkg/yurthub/otaupdate/upgrader/static_pod.go @@ -29,10 +29,10 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" - spctrlutil "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" upgrade "github.com/openyurtio/openyurt/pkg/node-servant/static-pod-upgrade" upgradeutil "github.com/openyurtio/openyurt/pkg/node-servant/static-pod-upgrade/util" "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate/util" + spctrlutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/util" ) const OTA = "OTA" diff --git a/pkg/yurthub/otaupdate/upgrader/static_pod_test.go b/pkg/yurthub/otaupdate/upgrader/static_pod_test.go index 9f05aba4267..33a01933597 100644 --- a/pkg/yurthub/otaupdate/upgrader/static_pod_test.go +++ b/pkg/yurthub/otaupdate/upgrader/static_pod_test.go @@ -27,10 +27,10 @@ import ( "k8s.io/apimachinery/pkg/util/rand" "k8s.io/client-go/kubernetes/fake" - spctrlutil "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" upgrade "github.com/openyurtio/openyurt/pkg/node-servant/static-pod-upgrade" upgradeutil "github.com/openyurtio/openyurt/pkg/node-servant/static-pod-upgrade/util" "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate/util" + spctrlutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/util" ) func TestStaticPodUpgrader_ApplyManifestNotExist(t *testing.T) { diff --git a/pkg/yurthub/otaupdate/util/util.go b/pkg/yurthub/otaupdate/util/util.go index c9481013819..fd8c16b4a93 100644 --- a/pkg/yurthub/otaupdate/util/util.go +++ b/pkg/yurthub/otaupdate/util/util.go @@ -27,8 +27,8 @@ import ( "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater" yurtutil "github.com/openyurtio/openyurt/pkg/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/daemonpodupdater" ) // Derived from kubelet encodePods diff --git a/pkg/yurthub/server/server.go b/pkg/yurthub/server/server.go index 4b7189ac4b6..1dff97ddf14 100644 --- a/pkg/yurthub/server/server.go +++ b/pkg/yurthub/server/server.go @@ -28,7 +28,7 @@ import ( "k8s.io/klog/v2" "github.com/openyurtio/openyurt/cmd/yurthub/app/config" - "github.com/openyurtio/openyurt/pkg/profile" + "github.com/openyurtio/openyurt/pkg/util/profile" "github.com/openyurtio/openyurt/pkg/yurthub/certificate" "github.com/openyurtio/openyurt/pkg/yurthub/kubernetes/rest" ota "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate" diff --git a/pkg/controller/apis/config/types.go b/pkg/yurtmanager/controller/apis/config/types.go similarity index 81% rename from pkg/controller/apis/config/types.go rename to pkg/yurtmanager/controller/apis/config/types.go index ef74904330c..3be8f661277 100644 --- a/pkg/controller/apis/config/types.go +++ b/pkg/yurtmanager/controller/apis/config/types.go @@ -19,12 +19,12 @@ package config import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - nodepoolconfig "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" - platformadminconfig "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" - gatewaypickupconfig "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" - yurtappdaemonconfig "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon/config" - yurtappsetconfig "github.com/openyurtio/openyurt/pkg/controller/yurtappset/config" - yurtstaticsetconfig "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/config" + nodepoolconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool/config" + platformadminconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" + gatewaypickupconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup/config" + yurtappdaemonconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon/config" + yurtappsetconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/config" + yurtstaticsetconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/config" ) // YurtManagerConfiguration contains elements describing yurt-manager. diff --git a/pkg/controller/controller.go b/pkg/yurtmanager/controller/controller.go similarity index 71% rename from pkg/controller/controller.go rename to pkg/yurtmanager/controller/controller.go index b9a81666c17..2df60ea13c1 100644 --- a/pkg/controller/controller.go +++ b/pkg/yurtmanager/controller/controller.go @@ -22,22 +22,22 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - "github.com/openyurtio/openyurt/pkg/controller/csrapprover" - "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater" - "github.com/openyurtio/openyurt/pkg/controller/nodepool" - "github.com/openyurtio/openyurt/pkg/controller/platformadmin" - "github.com/openyurtio/openyurt/pkg/controller/raven" - "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup" - "github.com/openyurtio/openyurt/pkg/controller/servicetopology" - servicetopologyendpoints "github.com/openyurtio/openyurt/pkg/controller/servicetopology/endpoints" - servicetopologyendpointslice "github.com/openyurtio/openyurt/pkg/controller/servicetopology/endpointslice" - "github.com/openyurtio/openyurt/pkg/controller/util" - "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon" - "github.com/openyurtio/openyurt/pkg/controller/yurtappset" - yurtcoordinatorcert "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/cert" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/delegatelease" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/podbinding" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/csrapprover" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/daemonpodupdater" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" + servicetopologyendpoints "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/endpoints" + servicetopologyendpointslice "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/endpointslice" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset" + yurtcoordinatorcert "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/cert" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/delegatelease" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/podbinding" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset" ) // Note !!! @kadisi diff --git a/pkg/controller/csrapprover/csrapprover_controller.go b/pkg/yurtmanager/controller/csrapprover/csrapprover_controller.go similarity index 99% rename from pkg/controller/csrapprover/csrapprover_controller.go rename to pkg/yurtmanager/controller/csrapprover/csrapprover_controller.go index ae4a65497a8..0316fb3d66a 100644 --- a/pkg/controller/csrapprover/csrapprover_controller.go +++ b/pkg/yurtmanager/controller/csrapprover/csrapprover_controller.go @@ -43,9 +43,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - yurtcoorrdinatorCert "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/cert" "github.com/openyurtio/openyurt/pkg/projectinfo" "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token" + yurtcoorrdinatorCert "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/cert" "github.com/openyurtio/openyurt/pkg/yurttunnel/constants" ) diff --git a/pkg/controller/csrapprover/csrapprover_controller_test.go b/pkg/yurtmanager/controller/csrapprover/csrapprover_controller_test.go similarity index 100% rename from pkg/controller/csrapprover/csrapprover_controller_test.go rename to pkg/yurtmanager/controller/csrapprover/csrapprover_controller_test.go diff --git a/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go b/pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller.go similarity index 99% rename from pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go rename to pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller.go index 95e27645a13..726acefc586 100644 --- a/pkg/controller/daemonpodupdater/daemon_pod_updater_controller.go +++ b/pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller.go @@ -49,7 +49,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - k8sutil "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater/kubernetes" + k8sutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/daemonpodupdater/kubernetes" ) func init() { diff --git a/pkg/controller/daemonpodupdater/daemon_pod_updater_controller_test.go b/pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller_test.go similarity index 99% rename from pkg/controller/daemonpodupdater/daemon_pod_updater_controller_test.go rename to pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller_test.go index 187caa2c377..744fd618a0d 100644 --- a/pkg/controller/daemonpodupdater/daemon_pod_updater_controller_test.go +++ b/pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller_test.go @@ -34,7 +34,7 @@ import ( fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" - k8sutil "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater/kubernetes" + k8sutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/daemonpodupdater/kubernetes" ) const ( diff --git a/pkg/controller/daemonpodupdater/kubernetes/controller_utils.go b/pkg/yurtmanager/controller/daemonpodupdater/kubernetes/controller_utils.go similarity index 100% rename from pkg/controller/daemonpodupdater/kubernetes/controller_utils.go rename to pkg/yurtmanager/controller/daemonpodupdater/kubernetes/controller_utils.go diff --git a/pkg/controller/daemonpodupdater/kubernetes/controller_utils_test.go b/pkg/yurtmanager/controller/daemonpodupdater/kubernetes/controller_utils_test.go similarity index 100% rename from pkg/controller/daemonpodupdater/kubernetes/controller_utils_test.go rename to pkg/yurtmanager/controller/daemonpodupdater/kubernetes/controller_utils_test.go diff --git a/pkg/controller/daemonpodupdater/kubernetes/pod_util.go b/pkg/yurtmanager/controller/daemonpodupdater/kubernetes/pod_util.go similarity index 100% rename from pkg/controller/daemonpodupdater/kubernetes/pod_util.go rename to pkg/yurtmanager/controller/daemonpodupdater/kubernetes/pod_util.go diff --git a/pkg/controller/daemonpodupdater/kubernetes/pod_util_test.go b/pkg/yurtmanager/controller/daemonpodupdater/kubernetes/pod_util_test.go similarity index 100% rename from pkg/controller/daemonpodupdater/kubernetes/pod_util_test.go rename to pkg/yurtmanager/controller/daemonpodupdater/kubernetes/pod_util_test.go diff --git a/pkg/controller/daemonpodupdater/util.go b/pkg/yurtmanager/controller/daemonpodupdater/util.go similarity index 96% rename from pkg/controller/daemonpodupdater/util.go rename to pkg/yurtmanager/controller/daemonpodupdater/util.go index 01a42536977..be7f5fe41e3 100644 --- a/pkg/controller/daemonpodupdater/util.go +++ b/pkg/yurtmanager/controller/daemonpodupdater/util.go @@ -29,8 +29,8 @@ import ( "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" - k8sutil "github.com/openyurtio/openyurt/pkg/controller/daemonpodupdater/kubernetes" - util "github.com/openyurtio/openyurt/pkg/controller/util/node" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/daemonpodupdater/kubernetes" + util "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" ) // GetDaemonsetPods get all pods belong to the given daemonset @@ -62,7 +62,7 @@ func GetDaemonsetPods(c client.Client, ds *appsv1.DaemonSet) ([]*corev1.Pod, err // IsDaemonsetPodLatest check whether pod is the latest by comparing its Spec with daemonset's // If pod is latest, return true, otherwise return false func IsDaemonsetPodLatest(ds *appsv1.DaemonSet, pod *corev1.Pod) bool { - hash := k8sutil.ComputeHash(&ds.Spec.Template, ds.Status.CollisionCount) + hash := kubernetes.ComputeHash(&ds.Spec.Template, ds.Status.CollisionCount) klog.V(4).Infof("compute hash: %v", hash) generation, err := GetTemplateGeneration(ds) if err != nil { @@ -243,6 +243,6 @@ func IsPodUpgradeConditionTrue(status corev1.PodStatus) bool { // GetPodUpgradeCondition extracts the pod upgrade condition from the given status and returns that. // Returns nil if the condition is not present. func GetPodUpgradeCondition(status corev1.PodStatus) *corev1.PodCondition { - _, condition := k8sutil.GetPodCondition(&status, PodNeedUpgrade) + _, condition := kubernetes.GetPodCondition(&status, PodNeedUpgrade) return condition } diff --git a/pkg/controller/daemonpodupdater/util_test.go b/pkg/yurtmanager/controller/daemonpodupdater/util_test.go similarity index 100% rename from pkg/controller/daemonpodupdater/util_test.go rename to pkg/yurtmanager/controller/daemonpodupdater/util_test.go diff --git a/pkg/controller/nodepool/config/types.go b/pkg/yurtmanager/controller/nodepool/config/types.go similarity index 100% rename from pkg/controller/nodepool/config/types.go rename to pkg/yurtmanager/controller/nodepool/config/types.go diff --git a/pkg/controller/nodepool/nodepool_controller.go b/pkg/yurtmanager/controller/nodepool/nodepool_controller.go similarity index 98% rename from pkg/controller/nodepool/nodepool_controller.go rename to pkg/yurtmanager/controller/nodepool/nodepool_controller.go index 2ca6d37ea35..e7372938c10 100644 --- a/pkg/controller/nodepool/nodepool_controller.go +++ b/pkg/yurtmanager/controller/nodepool/nodepool_controller.go @@ -35,7 +35,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" - poolconfig "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" + poolconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool/config" ) var ( diff --git a/pkg/controller/nodepool/nodepool_controller_test.go b/pkg/yurtmanager/controller/nodepool/nodepool_controller_test.go similarity index 98% rename from pkg/controller/nodepool/nodepool_controller_test.go rename to pkg/yurtmanager/controller/nodepool/nodepool_controller_test.go index aa0d0c687fb..dfff1242bee 100644 --- a/pkg/controller/nodepool/nodepool_controller_test.go +++ b/pkg/yurtmanager/controller/nodepool/nodepool_controller_test.go @@ -33,7 +33,7 @@ import ( "github.com/openyurtio/openyurt/pkg/apis" "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" - poolconfig "github.com/openyurtio/openyurt/pkg/controller/nodepool/config" + poolconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool/config" ) func prepareNodes() []client.Object { diff --git a/pkg/controller/nodepool/nodepool_enqueue_handlers.go b/pkg/yurtmanager/controller/nodepool/nodepool_enqueue_handlers.go similarity index 100% rename from pkg/controller/nodepool/nodepool_enqueue_handlers.go rename to pkg/yurtmanager/controller/nodepool/nodepool_enqueue_handlers.go diff --git a/pkg/controller/nodepool/nodepool_enqueue_handlers_test.go b/pkg/yurtmanager/controller/nodepool/nodepool_enqueue_handlers_test.go similarity index 100% rename from pkg/controller/nodepool/nodepool_enqueue_handlers_test.go rename to pkg/yurtmanager/controller/nodepool/nodepool_enqueue_handlers_test.go diff --git a/pkg/controller/nodepool/util.go b/pkg/yurtmanager/controller/nodepool/util.go similarity index 98% rename from pkg/controller/nodepool/util.go rename to pkg/yurtmanager/controller/nodepool/util.go index dd816cde1d8..c502723d530 100644 --- a/pkg/controller/nodepool/util.go +++ b/pkg/yurtmanager/controller/nodepool/util.go @@ -25,7 +25,7 @@ import ( "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" - nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" + nodeutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" ) // conciliatePoolRelatedAttrs will update the node's attributes that related to diff --git a/pkg/controller/nodepool/util_test.go b/pkg/yurtmanager/controller/nodepool/util_test.go similarity index 100% rename from pkg/controller/nodepool/util_test.go rename to pkg/yurtmanager/controller/nodepool/util_test.go diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json b/pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/config-nosecty.json similarity index 100% rename from pkg/controller/platformadmin/config/EdgeXConfig/config-nosecty.json rename to pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/config-nosecty.json diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/config.json b/pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/config.json similarity index 100% rename from pkg/controller/platformadmin/config/EdgeXConfig/config.json rename to pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/config.json diff --git a/pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml b/pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/manifest.yaml similarity index 100% rename from pkg/controller/platformadmin/config/EdgeXConfig/manifest.yaml rename to pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/manifest.yaml diff --git a/pkg/controller/platformadmin/config/types.go b/pkg/yurtmanager/controller/platformadmin/config/types.go similarity index 100% rename from pkg/controller/platformadmin/config/types.go rename to pkg/yurtmanager/controller/platformadmin/config/types.go diff --git a/pkg/controller/platformadmin/platformadmin_controller.go b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go similarity index 99% rename from pkg/controller/platformadmin/platformadmin_controller.go rename to pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go index 1edc362cfa1..2ee00160579 100644 --- a/pkg/controller/platformadmin/platformadmin_controller.go +++ b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go @@ -47,8 +47,8 @@ import ( appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" - "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" - util "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" + util "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/utils" ) func init() { diff --git a/pkg/controller/platformadmin/util.go b/pkg/yurtmanager/controller/platformadmin/util.go similarity index 95% rename from pkg/controller/platformadmin/util.go rename to pkg/yurtmanager/controller/platformadmin/util.go index 6abc2b5e780..9975c683083 100644 --- a/pkg/controller/platformadmin/util.go +++ b/pkg/yurtmanager/controller/platformadmin/util.go @@ -27,8 +27,8 @@ import ( "k8s.io/utils/pointer" iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" - "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" - utils "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" + utils "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/utils" ) // NewYurtIoTDockComponent initialize the configuration of yurt-iot-dock component diff --git a/pkg/controller/platformadmin/utils/conditions.go b/pkg/yurtmanager/controller/platformadmin/utils/conditions.go similarity index 100% rename from pkg/controller/platformadmin/utils/conditions.go rename to pkg/yurtmanager/controller/platformadmin/utils/conditions.go diff --git a/pkg/controller/platformadmin/utils/fieldindexer.go b/pkg/yurtmanager/controller/platformadmin/utils/fieldindexer.go similarity index 100% rename from pkg/controller/platformadmin/utils/fieldindexer.go rename to pkg/yurtmanager/controller/platformadmin/utils/fieldindexer.go diff --git a/pkg/controller/platformadmin/utils/version.go b/pkg/yurtmanager/controller/platformadmin/utils/version.go similarity index 100% rename from pkg/controller/platformadmin/utils/version.go rename to pkg/yurtmanager/controller/platformadmin/utils/version.go diff --git a/pkg/controller/raven/common.go b/pkg/yurtmanager/controller/raven/common.go similarity index 100% rename from pkg/controller/raven/common.go rename to pkg/yurtmanager/controller/raven/common.go diff --git a/pkg/controller/raven/gatewaypickup/config/types.go b/pkg/yurtmanager/controller/raven/gatewaypickup/config/types.go similarity index 100% rename from pkg/controller/raven/gatewaypickup/config/types.go rename to pkg/yurtmanager/controller/raven/gatewaypickup/config/types.go diff --git a/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go similarity index 97% rename from pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go rename to pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go index 24d8337a21a..2449cd775a3 100644 --- a/pkg/controller/raven/gatewaypickup/gateway_pickup_controller.go +++ b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go @@ -42,10 +42,10 @@ import ( appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" "github.com/openyurtio/openyurt/pkg/apis/raven" ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" - common "github.com/openyurtio/openyurt/pkg/controller/raven" - "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" - "github.com/openyurtio/openyurt/pkg/controller/raven/utils" - nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" + common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" + nodeutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" ) var ( diff --git a/pkg/controller/raven/gatewaypickup/gateway_pickup_controller_test.go b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller_test.go similarity index 98% rename from pkg/controller/raven/gatewaypickup/gateway_pickup_controller_test.go rename to pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller_test.go index c6586a2cc17..585d6f2fc23 100644 --- a/pkg/controller/raven/gatewaypickup/gateway_pickup_controller_test.go +++ b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller_test.go @@ -26,8 +26,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" - "github.com/openyurtio/openyurt/pkg/controller/raven/gatewaypickup/config" - "github.com/openyurtio/openyurt/pkg/controller/raven/utils" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" ) var ( diff --git a/pkg/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go similarity index 98% rename from pkg/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go rename to pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go index 26c57e14847..4d628735257 100644 --- a/pkg/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go +++ b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_enqueue_handlers.go @@ -27,7 +27,7 @@ import ( "github.com/openyurtio/openyurt/pkg/apis/raven" ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" - "github.com/openyurtio/openyurt/pkg/controller/raven/utils" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" ) type EnqueueGatewayForNode struct{} diff --git a/pkg/controller/raven/utils/utils.go b/pkg/yurtmanager/controller/raven/utils/utils.go similarity index 100% rename from pkg/controller/raven/utils/utils.go rename to pkg/yurtmanager/controller/raven/utils/utils.go diff --git a/pkg/controller/servicetopology/adapter/adapter.go b/pkg/yurtmanager/controller/servicetopology/adapter/adapter.go similarity index 100% rename from pkg/controller/servicetopology/adapter/adapter.go rename to pkg/yurtmanager/controller/servicetopology/adapter/adapter.go diff --git a/pkg/controller/servicetopology/adapter/endpoints_adapter.go b/pkg/yurtmanager/controller/servicetopology/adapter/endpoints_adapter.go similarity index 100% rename from pkg/controller/servicetopology/adapter/endpoints_adapter.go rename to pkg/yurtmanager/controller/servicetopology/adapter/endpoints_adapter.go diff --git a/pkg/controller/servicetopology/adapter/endpoints_adapter_test.go b/pkg/yurtmanager/controller/servicetopology/adapter/endpoints_adapter_test.go similarity index 100% rename from pkg/controller/servicetopology/adapter/endpoints_adapter_test.go rename to pkg/yurtmanager/controller/servicetopology/adapter/endpoints_adapter_test.go diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go b/pkg/yurtmanager/controller/servicetopology/adapter/endpointslicev1_adapter.go similarity index 100% rename from pkg/controller/servicetopology/adapter/endpointslicev1_adapter.go rename to pkg/yurtmanager/controller/servicetopology/adapter/endpointslicev1_adapter.go diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go b/pkg/yurtmanager/controller/servicetopology/adapter/endpointslicev1_adapter_test.go similarity index 100% rename from pkg/controller/servicetopology/adapter/endpointslicev1_adapter_test.go rename to pkg/yurtmanager/controller/servicetopology/adapter/endpointslicev1_adapter_test.go diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go b/pkg/yurtmanager/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go similarity index 100% rename from pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go rename to pkg/yurtmanager/controller/servicetopology/adapter/endpointslicev1beta1_adapter.go diff --git a/pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go b/pkg/yurtmanager/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go similarity index 100% rename from pkg/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go rename to pkg/yurtmanager/controller/servicetopology/adapter/endpointslicev1beta1_adapter_test.go diff --git a/pkg/controller/servicetopology/common.go b/pkg/yurtmanager/controller/servicetopology/common.go similarity index 100% rename from pkg/controller/servicetopology/common.go rename to pkg/yurtmanager/controller/servicetopology/common.go diff --git a/pkg/controller/servicetopology/endpoints/endpoints_controller.go b/pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_controller.go similarity index 96% rename from pkg/controller/servicetopology/endpoints/endpoints_controller.go rename to pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_controller.go index e35426e662f..39adacb3088 100644 --- a/pkg/controller/servicetopology/endpoints/endpoints_controller.go +++ b/pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_controller.go @@ -31,8 +31,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - common "github.com/openyurtio/openyurt/pkg/controller/servicetopology" - "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" + common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/adapter" ) func init() { diff --git a/pkg/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go b/pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go similarity index 93% rename from pkg/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go rename to pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go index 2ac0c355fb6..febf3d35a51 100644 --- a/pkg/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go +++ b/pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_enqueue_handlers.go @@ -25,8 +25,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" - "github.com/openyurtio/openyurt/pkg/controller/servicetopology/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/adapter" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/util" ) type EnqueueEndpointsForService struct { diff --git a/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go b/pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_controller.go similarity index 96% rename from pkg/controller/servicetopology/endpointslice/endpointslice_controller.go rename to pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_controller.go index 335b2ccbbd8..8a26925f55f 100644 --- a/pkg/controller/servicetopology/endpointslice/endpointslice_controller.go +++ b/pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_controller.go @@ -34,8 +34,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - common "github.com/openyurtio/openyurt/pkg/controller/servicetopology" - "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" + common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/adapter" ) func init() { diff --git a/pkg/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go b/pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go similarity index 93% rename from pkg/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go rename to pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go index 1d39a4c5f56..234af1bd95d 100644 --- a/pkg/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go +++ b/pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_enqueue_handlers.go @@ -25,8 +25,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/openyurtio/openyurt/pkg/controller/servicetopology/adapter" - "github.com/openyurtio/openyurt/pkg/controller/servicetopology/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/adapter" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/util" ) type EnqueueEndpointsliceForService struct { diff --git a/pkg/controller/servicetopology/util/util.go b/pkg/yurtmanager/controller/servicetopology/util/util.go similarity index 100% rename from pkg/controller/servicetopology/util/util.go rename to pkg/yurtmanager/controller/servicetopology/util/util.go diff --git a/pkg/controller/util/controller_utils.go b/pkg/yurtmanager/controller/util/controller_utils.go similarity index 100% rename from pkg/controller/util/controller_utils.go rename to pkg/yurtmanager/controller/util/controller_utils.go diff --git a/pkg/controller/util/controller_utils_test.go b/pkg/yurtmanager/controller/util/controller_utils_test.go similarity index 100% rename from pkg/controller/util/controller_utils_test.go rename to pkg/yurtmanager/controller/util/controller_utils_test.go diff --git a/pkg/controller/util/node/controller_utils.go b/pkg/yurtmanager/controller/util/node/controller_utils.go similarity index 100% rename from pkg/controller/util/node/controller_utils.go rename to pkg/yurtmanager/controller/util/node/controller_utils.go diff --git a/pkg/controller/util/refmanager/ref_manager.go b/pkg/yurtmanager/controller/util/refmanager/ref_manager.go similarity index 100% rename from pkg/controller/util/refmanager/ref_manager.go rename to pkg/yurtmanager/controller/util/refmanager/ref_manager.go diff --git a/pkg/controller/util/refmanager/ref_manager_test.go b/pkg/yurtmanager/controller/util/refmanager/ref_manager_test.go similarity index 100% rename from pkg/controller/util/refmanager/ref_manager_test.go rename to pkg/yurtmanager/controller/util/refmanager/ref_manager_test.go diff --git a/pkg/controller/util/tools.go b/pkg/yurtmanager/controller/util/tools.go similarity index 100% rename from pkg/controller/util/tools.go rename to pkg/yurtmanager/controller/util/tools.go diff --git a/pkg/controller/util/tools_test.go b/pkg/yurtmanager/controller/util/tools_test.go similarity index 100% rename from pkg/controller/util/tools_test.go rename to pkg/yurtmanager/controller/util/tools_test.go diff --git a/pkg/controller/yurtappdaemon/config/types.go b/pkg/yurtmanager/controller/yurtappdaemon/config/types.go similarity index 100% rename from pkg/controller/yurtappdaemon/config/types.go rename to pkg/yurtmanager/controller/yurtappdaemon/config/types.go diff --git a/pkg/controller/yurtappdaemon/nodepool_enqueue_handlers.go b/pkg/yurtmanager/controller/yurtappdaemon/nodepool_enqueue_handlers.go similarity index 100% rename from pkg/controller/yurtappdaemon/nodepool_enqueue_handlers.go rename to pkg/yurtmanager/controller/yurtappdaemon/nodepool_enqueue_handlers.go diff --git a/pkg/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go b/pkg/yurtmanager/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go similarity index 100% rename from pkg/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go rename to pkg/yurtmanager/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go diff --git a/pkg/controller/yurtappdaemon/revision.go b/pkg/yurtmanager/controller/yurtappdaemon/revision.go similarity index 99% rename from pkg/controller/yurtappdaemon/revision.go rename to pkg/yurtmanager/controller/yurtappdaemon/revision.go index 6e1bb4b8e57..e05a990c8bf 100644 --- a/pkg/controller/yurtappdaemon/revision.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/revision.go @@ -32,7 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/apiutil" appsalphav1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/util/refmanager" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/refmanager" ) func (r *ReconcileYurtAppDaemon) controlledHistories(yad *appsalphav1.YurtAppDaemon) ([]*apps.ControllerRevision, error) { diff --git a/pkg/controller/yurtappdaemon/revision_test.go b/pkg/yurtmanager/controller/yurtappdaemon/revision_test.go similarity index 100% rename from pkg/controller/yurtappdaemon/revision_test.go rename to pkg/yurtmanager/controller/yurtappdaemon/revision_test.go diff --git a/pkg/controller/yurtappdaemon/util.go b/pkg/yurtmanager/controller/yurtappdaemon/util.go similarity index 100% rename from pkg/controller/yurtappdaemon/util.go rename to pkg/yurtmanager/controller/yurtappdaemon/util.go diff --git a/pkg/controller/yurtappdaemon/util_test.go b/pkg/yurtmanager/controller/yurtappdaemon/util_test.go similarity index 100% rename from pkg/controller/yurtappdaemon/util_test.go rename to pkg/yurtmanager/controller/yurtappdaemon/util_test.go diff --git a/pkg/controller/yurtappdaemon/workloadcontroller/controller.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/controller.go similarity index 100% rename from pkg/controller/yurtappdaemon/workloadcontroller/controller.go rename to pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/controller.go diff --git a/pkg/controller/yurtappdaemon/workloadcontroller/deployment_controller.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller.go similarity index 98% rename from pkg/controller/yurtappdaemon/workloadcontroller/deployment_controller.go rename to pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller.go index 0e3fb0dc27b..e2985991a4d 100644 --- a/pkg/controller/yurtappdaemon/workloadcontroller/deployment_controller.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller.go @@ -29,7 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/util/refmanager" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/refmanager" ) const updateRetries = 5 diff --git a/pkg/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go similarity index 100% rename from pkg/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go rename to pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go diff --git a/pkg/controller/yurtappdaemon/workloadcontroller/statefulset_controller.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/statefulset_controller.go similarity index 100% rename from pkg/controller/yurtappdaemon/workloadcontroller/statefulset_controller.go rename to pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/statefulset_controller.go diff --git a/pkg/controller/yurtappdaemon/workloadcontroller/util.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util.go similarity index 100% rename from pkg/controller/yurtappdaemon/workloadcontroller/util.go rename to pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util.go diff --git a/pkg/controller/yurtappdaemon/workloadcontroller/util_test.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util_test.go similarity index 100% rename from pkg/controller/yurtappdaemon/workloadcontroller/util_test.go rename to pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util_test.go diff --git a/pkg/controller/yurtappdaemon/workloadcontroller/workload.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload.go similarity index 100% rename from pkg/controller/yurtappdaemon/workloadcontroller/workload.go rename to pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload.go diff --git a/pkg/controller/yurtappdaemon/workloadcontroller/workload_test.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload_test.go similarity index 100% rename from pkg/controller/yurtappdaemon/workloadcontroller/workload_test.go rename to pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload_test.go diff --git a/pkg/controller/yurtappdaemon/yurtappdaemon_controller.go b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller.go similarity index 99% rename from pkg/controller/yurtappdaemon/yurtappdaemon_controller.go rename to pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller.go index 0434ec7b70f..097a092ef07 100644 --- a/pkg/controller/yurtappdaemon/yurtappdaemon_controller.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller.go @@ -39,8 +39,8 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/util" - "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon/workloadcontroller" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller" ) var ( diff --git a/pkg/controller/yurtappdaemon/yurtappdaemon_controller_test.go b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go similarity index 99% rename from pkg/controller/yurtappdaemon/yurtappdaemon_controller_test.go rename to pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go index 0bc6a05c193..9a668ac6963 100644 --- a/pkg/controller/yurtappdaemon/yurtappdaemon_controller_test.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go @@ -26,7 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon/workloadcontroller" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller" ) //func TestAdd(t *testing.T) { diff --git a/pkg/controller/yurtappset/adapter/adapter.go b/pkg/yurtmanager/controller/yurtappset/adapter/adapter.go similarity index 100% rename from pkg/controller/yurtappset/adapter/adapter.go rename to pkg/yurtmanager/controller/yurtappset/adapter/adapter.go diff --git a/pkg/controller/yurtappset/adapter/adapter_util.go b/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util.go similarity index 100% rename from pkg/controller/yurtappset/adapter/adapter_util.go rename to pkg/yurtmanager/controller/yurtappset/adapter/adapter_util.go diff --git a/pkg/controller/yurtappset/adapter/adapter_util_test.go b/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util_test.go similarity index 100% rename from pkg/controller/yurtappset/adapter/adapter_util_test.go rename to pkg/yurtmanager/controller/yurtappset/adapter/adapter_util_test.go diff --git a/pkg/controller/yurtappset/adapter/deployment_adapter.go b/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter.go similarity index 100% rename from pkg/controller/yurtappset/adapter/deployment_adapter.go rename to pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter.go diff --git a/pkg/controller/yurtappset/adapter/deployment_adapter_test.go b/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter_test.go similarity index 100% rename from pkg/controller/yurtappset/adapter/deployment_adapter_test.go rename to pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter_test.go diff --git a/pkg/controller/yurtappset/adapter/statefulset_adapter.go b/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter.go similarity index 100% rename from pkg/controller/yurtappset/adapter/statefulset_adapter.go rename to pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter.go diff --git a/pkg/controller/yurtappset/adapter/statefulset_adapter_test.go b/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter_test.go similarity index 100% rename from pkg/controller/yurtappset/adapter/statefulset_adapter_test.go rename to pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter_test.go diff --git a/pkg/controller/yurtappset/config/types.go b/pkg/yurtmanager/controller/yurtappset/config/types.go similarity index 100% rename from pkg/controller/yurtappset/config/types.go rename to pkg/yurtmanager/controller/yurtappset/config/types.go diff --git a/pkg/controller/yurtappset/pool.go b/pkg/yurtmanager/controller/yurtappset/pool.go similarity index 96% rename from pkg/controller/yurtappset/pool.go rename to pkg/yurtmanager/controller/yurtappset/pool.go index 4d0a7c0b1cf..a579a8f3a2f 100644 --- a/pkg/controller/yurtappset/pool.go +++ b/pkg/yurtmanager/controller/yurtappset/pool.go @@ -21,7 +21,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/yurtappset/adapter" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/adapter" ) // Pool stores the details of a pool resource owned by one YurtAppSet. diff --git a/pkg/controller/yurtappset/pool_control.go b/pkg/yurtmanager/controller/yurtappset/pool_control.go similarity index 97% rename from pkg/controller/yurtappset/pool_control.go rename to pkg/yurtmanager/controller/yurtappset/pool_control.go index 98530e74f42..34791df33bf 100644 --- a/pkg/controller/yurtappset/pool_control.go +++ b/pkg/yurtmanager/controller/yurtappset/pool_control.go @@ -29,8 +29,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/util/refmanager" - "github.com/openyurtio/openyurt/pkg/controller/yurtappset/adapter" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/refmanager" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/adapter" ) // PoolControl provides pool operations of MutableSet. diff --git a/pkg/controller/yurtappset/pool_controller_test.go b/pkg/yurtmanager/controller/yurtappset/pool_controller_test.go similarity index 98% rename from pkg/controller/yurtappset/pool_controller_test.go rename to pkg/yurtmanager/controller/yurtappset/pool_controller_test.go index 9a79e5e2278..da2c8e0ac5a 100644 --- a/pkg/controller/yurtappset/pool_controller_test.go +++ b/pkg/yurtmanager/controller/yurtappset/pool_controller_test.go @@ -29,7 +29,7 @@ import ( fakeclint "sigs.k8s.io/controller-runtime/pkg/client/fake" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - adpt "github.com/openyurtio/openyurt/pkg/controller/yurtappset/adapter" + adpt "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/adapter" ) var ( diff --git a/pkg/controller/yurtappset/revision.go b/pkg/yurtmanager/controller/yurtappset/revision.go similarity index 99% rename from pkg/controller/yurtappset/revision.go rename to pkg/yurtmanager/controller/yurtappset/revision.go index 35b33f338cb..987e72e8231 100644 --- a/pkg/controller/yurtappset/revision.go +++ b/pkg/yurtmanager/controller/yurtappset/revision.go @@ -34,7 +34,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/apiutil" appsalphav1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/util/refmanager" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/refmanager" ) // ControllerRevisionHashLabel is the label used to indicate the hash value of a ControllerRevision's Data. diff --git a/pkg/controller/yurtappset/revision_test.go b/pkg/yurtmanager/controller/yurtappset/revision_test.go similarity index 100% rename from pkg/controller/yurtappset/revision_test.go rename to pkg/yurtmanager/controller/yurtappset/revision_test.go diff --git a/pkg/controller/yurtappset/yurtappset_controller.go b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller.go similarity index 99% rename from pkg/controller/yurtappset/yurtappset_controller.go rename to pkg/yurtmanager/controller/yurtappset/yurtappset_controller.go index eeeb4c79e49..2d3edaa2dd5 100644 --- a/pkg/controller/yurtappset/yurtappset_controller.go +++ b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller.go @@ -42,7 +42,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/yurtappset/adapter" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/adapter" ) func init() { diff --git a/pkg/controller/yurtappset/yurtappset_controller_statefulset_test.go b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_statefulset_test.go similarity index 100% rename from pkg/controller/yurtappset/yurtappset_controller_statefulset_test.go rename to pkg/yurtmanager/controller/yurtappset/yurtappset_controller_statefulset_test.go diff --git a/pkg/controller/yurtappset/yurtappset_controller_suite_test.go b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_suite_test.go similarity index 100% rename from pkg/controller/yurtappset/yurtappset_controller_suite_test.go rename to pkg/yurtmanager/controller/yurtappset/yurtappset_controller_suite_test.go diff --git a/pkg/controller/yurtappset/yurtappset_controller_test.go b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_test.go similarity index 97% rename from pkg/controller/yurtappset/yurtappset_controller_test.go rename to pkg/yurtmanager/controller/yurtappset/yurtappset_controller_test.go index 3ebfbc26940..18a2a364a10 100644 --- a/pkg/controller/yurtappset/yurtappset_controller_test.go +++ b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_test.go @@ -32,7 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - adpt "github.com/openyurtio/openyurt/pkg/controller/yurtappset/adapter" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/adapter" ) var ( @@ -150,12 +150,12 @@ func TestReconcileYurtAppSet_Reconcile(t *testing.T) { appsv1alpha1.StatefulSetTemplateType: &PoolControl{ Client: fc, scheme: scheme, - adapter: &adpt.StatefulSetAdapter{Client: fc, Scheme: scheme}, + adapter: &adapter.StatefulSetAdapter{Client: fc, Scheme: scheme}, }, appsv1alpha1.DeploymentTemplateType: &PoolControl{ Client: fc, scheme: scheme, - adapter: &adpt.DeploymentAdapter{Client: fc, Scheme: scheme}, + adapter: &adapter.DeploymentAdapter{Client: fc, Scheme: scheme}, }, }, } diff --git a/pkg/controller/yurtappset/yurtappset_controller_utils.go b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_utils.go similarity index 100% rename from pkg/controller/yurtappset/yurtappset_controller_utils.go rename to pkg/yurtmanager/controller/yurtappset/yurtappset_controller_utils.go diff --git a/pkg/controller/yurtappset/yurtappset_update.go b/pkg/yurtmanager/controller/yurtappset/yurtappset_update.go similarity index 99% rename from pkg/controller/yurtappset/yurtappset_update.go rename to pkg/yurtmanager/controller/yurtappset/yurtappset_update.go index acfaa4f4999..6a73afd915b 100644 --- a/pkg/controller/yurtappset/yurtappset_update.go +++ b/pkg/yurtmanager/controller/yurtappset/yurtappset_update.go @@ -32,7 +32,7 @@ import ( "k8s.io/klog/v2" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util" ) func (r *ReconcileYurtAppSet) managePools(yas *unitv1alpha1.YurtAppSet, diff --git a/pkg/controller/yurtcoordinator/cert/certificate.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/certificate.go similarity index 100% rename from pkg/controller/yurtcoordinator/cert/certificate.go rename to pkg/yurtmanager/controller/yurtcoordinator/cert/certificate.go diff --git a/pkg/controller/yurtcoordinator/cert/certificate_test.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/certificate_test.go similarity index 100% rename from pkg/controller/yurtcoordinator/cert/certificate_test.go rename to pkg/yurtmanager/controller/yurtcoordinator/cert/certificate_test.go diff --git a/pkg/controller/yurtcoordinator/cert/secret.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/secret.go similarity index 100% rename from pkg/controller/yurtcoordinator/cert/secret.go rename to pkg/yurtmanager/controller/yurtcoordinator/cert/secret.go diff --git a/pkg/controller/yurtcoordinator/cert/secret_test.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/secret_test.go similarity index 100% rename from pkg/controller/yurtcoordinator/cert/secret_test.go rename to pkg/yurtmanager/controller/yurtcoordinator/cert/secret_test.go diff --git a/pkg/controller/yurtcoordinator/cert/util.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/util.go similarity index 100% rename from pkg/controller/yurtcoordinator/cert/util.go rename to pkg/yurtmanager/controller/yurtcoordinator/cert/util.go diff --git a/pkg/controller/yurtcoordinator/cert/util_test.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/util_test.go similarity index 100% rename from pkg/controller/yurtcoordinator/cert/util_test.go rename to pkg/yurtmanager/controller/yurtcoordinator/cert/util_test.go diff --git a/pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go similarity index 100% rename from pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go rename to pkg/yurtmanager/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go diff --git a/pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go similarity index 100% rename from pkg/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go rename to pkg/yurtmanager/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller_test.go diff --git a/pkg/controller/yurtcoordinator/constant/constant.go b/pkg/yurtmanager/controller/yurtcoordinator/constant/constant.go similarity index 100% rename from pkg/controller/yurtcoordinator/constant/constant.go rename to pkg/yurtmanager/controller/yurtcoordinator/constant/constant.go diff --git a/pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller.go b/pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller.go similarity index 96% rename from pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller.go rename to pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller.go index ea5f68f2886..7420f6007a1 100644 --- a/pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller.go +++ b/pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller.go @@ -36,9 +36,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - nodeutil "github.com/openyurtio/openyurt/pkg/controller/util/node" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/utils" + nodeutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/utils" ) func init() { diff --git a/pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go b/pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go similarity index 93% rename from pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go rename to pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go index 037fd9c6fb3..57ada1cf45e 100644 --- a/pkg/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go +++ b/pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller_test.go @@ -21,7 +21,7 @@ import ( corev1 "k8s.io/api/core/v1" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/utils" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/utils" ) func TestTaintNode(t *testing.T) { diff --git a/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go b/pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller.go similarity index 98% rename from pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go rename to pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller.go index 02cfd571e07..cdd02666f58 100644 --- a/pkg/controller/yurtcoordinator/podbinding/podbinding_controller.go +++ b/pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller.go @@ -33,8 +33,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" "github.com/openyurtio/openyurt/pkg/projectinfo" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/constant" ) func init() { diff --git a/pkg/controller/yurtcoordinator/podbinding/podbinding_controller_test.go b/pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller_test.go similarity index 100% rename from pkg/controller/yurtcoordinator/podbinding/podbinding_controller_test.go rename to pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller_test.go diff --git a/pkg/controller/yurtcoordinator/utils/lease.go b/pkg/yurtmanager/controller/yurtcoordinator/utils/lease.go similarity index 94% rename from pkg/controller/yurtcoordinator/utils/lease.go rename to pkg/yurtmanager/controller/yurtcoordinator/utils/lease.go index 188fd1f49d7..2af5532d7d6 100644 --- a/pkg/controller/yurtcoordinator/utils/lease.go +++ b/pkg/yurtmanager/controller/yurtcoordinator/utils/lease.go @@ -20,7 +20,7 @@ package utils import ( "sync" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/constant" ) type LeaseDelegatedCounter struct { diff --git a/pkg/controller/yurtcoordinator/utils/lease_test.go b/pkg/yurtmanager/controller/yurtcoordinator/utils/lease_test.go similarity index 100% rename from pkg/controller/yurtcoordinator/utils/lease_test.go rename to pkg/yurtmanager/controller/yurtcoordinator/utils/lease_test.go diff --git a/pkg/controller/yurtcoordinator/utils/taints.go b/pkg/yurtmanager/controller/yurtcoordinator/utils/taints.go similarity index 100% rename from pkg/controller/yurtcoordinator/utils/taints.go rename to pkg/yurtmanager/controller/yurtcoordinator/utils/taints.go diff --git a/pkg/controller/yurtcoordinator/utils/taints_test.go b/pkg/yurtmanager/controller/yurtcoordinator/utils/taints_test.go similarity index 93% rename from pkg/controller/yurtcoordinator/utils/taints_test.go rename to pkg/yurtmanager/controller/yurtcoordinator/utils/taints_test.go index 46e15e740e1..4c052ba1110 100644 --- a/pkg/controller/yurtcoordinator/utils/taints_test.go +++ b/pkg/yurtmanager/controller/yurtcoordinator/utils/taints_test.go @@ -21,7 +21,7 @@ import ( v1 "k8s.io/api/core/v1" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/constant" ) func TestDeleteTaintsByKey(t *testing.T) { diff --git a/pkg/controller/yurtstaticset/config/types.go b/pkg/yurtmanager/controller/yurtstaticset/config/types.go similarity index 100% rename from pkg/controller/yurtstaticset/config/types.go rename to pkg/yurtmanager/controller/yurtstaticset/config/types.go diff --git a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go b/pkg/yurtmanager/controller/yurtstaticset/upgradeinfo/upgrade_info.go similarity index 99% rename from pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go rename to pkg/yurtmanager/controller/yurtstaticset/upgradeinfo/upgrade_info.go index be4d1476dbf..6027fe708c1 100644 --- a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info.go +++ b/pkg/yurtmanager/controller/yurtstaticset/upgradeinfo/upgrade_info.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/util" ) const ( diff --git a/pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go b/pkg/yurtmanager/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go similarity index 100% rename from pkg/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go rename to pkg/yurtmanager/controller/yurtstaticset/upgradeinfo/upgrade_info_test.go diff --git a/pkg/controller/yurtstaticset/util/util.go b/pkg/yurtmanager/controller/yurtstaticset/util/util.go similarity index 100% rename from pkg/controller/yurtstaticset/util/util.go rename to pkg/yurtmanager/controller/yurtstaticset/util/util.go diff --git a/pkg/controller/yurtstaticset/yurtstaticset_controller.go b/pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller.go similarity index 98% rename from pkg/controller/yurtstaticset/yurtstaticset_controller.go rename to pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller.go index 2047b64a051..16e76fb9092 100644 --- a/pkg/controller/yurtstaticset/yurtstaticset_controller.go +++ b/pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller.go @@ -42,9 +42,9 @@ import ( appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/config" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/upgradeinfo" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/upgradeinfo" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/util" ) func init() { diff --git a/pkg/controller/yurtstaticset/yurtstaticset_controller_test.go b/pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller_test.go similarity index 98% rename from pkg/controller/yurtstaticset/yurtstaticset_controller_test.go rename to pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller_test.go index 76eece0d502..4508aa78b43 100644 --- a/pkg/controller/yurtstaticset/yurtstaticset_controller_test.go +++ b/pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller_test.go @@ -32,7 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/util" ) const ( diff --git a/pkg/webhook/builder/defaulter_custom.go b/pkg/yurtmanager/webhook/builder/defaulter_custom.go similarity index 100% rename from pkg/webhook/builder/defaulter_custom.go rename to pkg/yurtmanager/webhook/builder/defaulter_custom.go diff --git a/pkg/webhook/builder/validator_custom.go b/pkg/yurtmanager/webhook/builder/validator_custom.go similarity index 100% rename from pkg/webhook/builder/validator_custom.go rename to pkg/yurtmanager/webhook/builder/validator_custom.go diff --git a/pkg/webhook/builder/webhook.go b/pkg/yurtmanager/webhook/builder/webhook.go similarity index 98% rename from pkg/webhook/builder/webhook.go rename to pkg/yurtmanager/webhook/builder/webhook.go index 240c7694617..a70bcbe4833 100644 --- a/pkg/webhook/builder/webhook.go +++ b/pkg/yurtmanager/webhook/builder/webhook.go @@ -30,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) // WebhookBuilder builds a Webhook. diff --git a/pkg/webhook/gateway/v1alpha1/gateway_default.go b/pkg/yurtmanager/webhook/gateway/v1alpha1/gateway_default.go similarity index 100% rename from pkg/webhook/gateway/v1alpha1/gateway_default.go rename to pkg/yurtmanager/webhook/gateway/v1alpha1/gateway_default.go diff --git a/pkg/webhook/gateway/v1alpha1/gateway_handler.go b/pkg/yurtmanager/webhook/gateway/v1alpha1/gateway_handler.go similarity index 96% rename from pkg/webhook/gateway/v1alpha1/gateway_handler.go rename to pkg/yurtmanager/webhook/gateway/v1alpha1/gateway_handler.go index 97573b424a4..d2f0b5272fc 100644 --- a/pkg/webhook/gateway/v1alpha1/gateway_handler.go +++ b/pkg/yurtmanager/webhook/gateway/v1alpha1/gateway_handler.go @@ -23,7 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/openyurtio/openyurt/pkg/apis/raven/v1alpha1" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) // SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error diff --git a/pkg/webhook/gateway/v1alpha1/gateway_validation.go b/pkg/yurtmanager/webhook/gateway/v1alpha1/gateway_validation.go similarity index 100% rename from pkg/webhook/gateway/v1alpha1/gateway_validation.go rename to pkg/yurtmanager/webhook/gateway/v1alpha1/gateway_validation.go diff --git a/pkg/webhook/gateway/v1beta1/gateway_default.go b/pkg/yurtmanager/webhook/gateway/v1beta1/gateway_default.go similarity index 100% rename from pkg/webhook/gateway/v1beta1/gateway_default.go rename to pkg/yurtmanager/webhook/gateway/v1beta1/gateway_default.go diff --git a/pkg/webhook/gateway/v1beta1/gateway_handler.go b/pkg/yurtmanager/webhook/gateway/v1beta1/gateway_handler.go similarity index 97% rename from pkg/webhook/gateway/v1beta1/gateway_handler.go rename to pkg/yurtmanager/webhook/gateway/v1beta1/gateway_handler.go index 1cd33b5540a..0193b8c9e6c 100644 --- a/pkg/webhook/gateway/v1beta1/gateway_handler.go +++ b/pkg/yurtmanager/webhook/gateway/v1beta1/gateway_handler.go @@ -23,7 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) // SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error diff --git a/pkg/webhook/gateway/v1beta1/gateway_validation.go b/pkg/yurtmanager/webhook/gateway/v1beta1/gateway_validation.go similarity index 100% rename from pkg/webhook/gateway/v1beta1/gateway_validation.go rename to pkg/yurtmanager/webhook/gateway/v1beta1/gateway_validation.go diff --git a/pkg/webhook/node/v1/node_default.go b/pkg/yurtmanager/webhook/node/v1/node_default.go similarity index 100% rename from pkg/webhook/node/v1/node_default.go rename to pkg/yurtmanager/webhook/node/v1/node_default.go diff --git a/pkg/webhook/node/v1/node_default_test.go b/pkg/yurtmanager/webhook/node/v1/node_default_test.go similarity index 100% rename from pkg/webhook/node/v1/node_default_test.go rename to pkg/yurtmanager/webhook/node/v1/node_default_test.go diff --git a/pkg/webhook/node/v1/node_handler.go b/pkg/yurtmanager/webhook/node/v1/node_handler.go similarity index 93% rename from pkg/webhook/node/v1/node_handler.go rename to pkg/yurtmanager/webhook/node/v1/node_handler.go index f6cbc5b6aa0..e63bc1730a3 100644 --- a/pkg/webhook/node/v1/node_handler.go +++ b/pkg/yurtmanager/webhook/node/v1/node_handler.go @@ -22,8 +22,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - "github.com/openyurtio/openyurt/pkg/webhook/builder" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/builder" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) const ( diff --git a/pkg/webhook/node/v1/node_validation.go b/pkg/yurtmanager/webhook/node/v1/node_validation.go similarity index 100% rename from pkg/webhook/node/v1/node_validation.go rename to pkg/yurtmanager/webhook/node/v1/node_validation.go diff --git a/pkg/webhook/node/v1/node_validation_test.go b/pkg/yurtmanager/webhook/node/v1/node_validation_test.go similarity index 100% rename from pkg/webhook/node/v1/node_validation_test.go rename to pkg/yurtmanager/webhook/node/v1/node_validation_test.go diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_default.go b/pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_default.go similarity index 100% rename from pkg/webhook/nodepool/v1beta1/nodepool_default.go rename to pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_default.go diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_default_test.go b/pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_default_test.go similarity index 100% rename from pkg/webhook/nodepool/v1beta1/nodepool_default_test.go rename to pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_default_test.go diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_handler.go b/pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_handler.go similarity index 97% rename from pkg/webhook/nodepool/v1beta1/nodepool_handler.go rename to pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_handler.go index d1fa643d5f7..0bf0f3f876f 100644 --- a/pkg/webhook/nodepool/v1beta1/nodepool_handler.go +++ b/pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_handler.go @@ -23,7 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) // SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_validation.go b/pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_validation.go similarity index 95% rename from pkg/webhook/nodepool/v1beta1/nodepool_validation.go rename to pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_validation.go index 81f7959d1a6..5c14370f339 100644 --- a/pkg/webhook/nodepool/v1beta1/nodepool_validation.go +++ b/pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_validation.go @@ -105,6 +105,11 @@ func validateNodePoolSpec(spec *appsv1beta1.NodePoolSpec) field.ErrorList { if spec.Type != appsv1beta1.Edge && spec.Type != appsv1beta1.Cloud { return []*field.Error{field.Invalid(field.NewPath("spec").Child("type"), spec.Type, "pool type should be Edge or Cloud")} } + + // Cloud NodePool can not set HostNetwork=true + if spec.Type == appsv1beta1.Cloud && spec.HostNetwork { + return []*field.Error{field.Invalid(field.NewPath("spec").Child("hostNetwork"), spec.HostNetwork, "Cloud NodePool cloud not support hostNetwork")} + } return nil } diff --git a/pkg/webhook/nodepool/v1beta1/nodepool_validation_test.go b/pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_validation_test.go similarity index 100% rename from pkg/webhook/nodepool/v1beta1/nodepool_validation_test.go rename to pkg/yurtmanager/webhook/nodepool/v1beta1/nodepool_validation_test.go diff --git a/pkg/webhook/platformadmin/v1alpha1/platformadmin_default.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_default.go similarity index 100% rename from pkg/webhook/platformadmin/v1alpha1/platformadmin_default.go rename to pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_default.go diff --git a/pkg/webhook/platformadmin/v1alpha1/platformadmin_handler.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_handler.go similarity index 94% rename from pkg/webhook/platformadmin/v1alpha1/platformadmin_handler.go rename to pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_handler.go index 83a7e4e3e92..7dc5b2e6170 100644 --- a/pkg/webhook/platformadmin/v1alpha1/platformadmin_handler.go +++ b/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_handler.go @@ -25,8 +25,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) type Manifest struct { diff --git a/pkg/webhook/platformadmin/v1alpha1/platformadmin_validation.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_validation.go similarity index 98% rename from pkg/webhook/platformadmin/v1alpha1/platformadmin_validation.go rename to pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_validation.go index 538017c4628..fa4236bf794 100644 --- a/pkg/webhook/platformadmin/v1alpha1/platformadmin_validation.go +++ b/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_validation.go @@ -27,7 +27,7 @@ import ( unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" - util "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" + util "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/utils" ) // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. diff --git a/pkg/webhook/platformadmin/v1alpha2/platformadmin_default.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_default.go similarity index 100% rename from pkg/webhook/platformadmin/v1alpha2/platformadmin_default.go rename to pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_default.go diff --git a/pkg/webhook/platformadmin/v1alpha2/platformadmin_handler.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_handler.go similarity index 95% rename from pkg/webhook/platformadmin/v1alpha2/platformadmin_handler.go rename to pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_handler.go index a6b193a0b74..aa707e3e879 100644 --- a/pkg/webhook/platformadmin/v1alpha2/platformadmin_handler.go +++ b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_handler.go @@ -25,8 +25,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" - "github.com/openyurtio/openyurt/pkg/controller/platformadmin/config" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) type Manifest struct { diff --git a/pkg/webhook/platformadmin/v1alpha2/platformadmin_validation.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_validation.go similarity index 98% rename from pkg/webhook/platformadmin/v1alpha2/platformadmin_validation.go rename to pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_validation.go index 170c790244a..8525c13db6d 100644 --- a/pkg/webhook/platformadmin/v1alpha2/platformadmin_validation.go +++ b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_validation.go @@ -28,7 +28,7 @@ import ( unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" - util "github.com/openyurtio/openyurt/pkg/controller/platformadmin/utils" + util "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/utils" ) // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. diff --git a/pkg/webhook/pod/v1/pod_handler.go b/pkg/yurtmanager/webhook/pod/v1/pod_handler.go similarity index 92% rename from pkg/webhook/pod/v1/pod_handler.go rename to pkg/yurtmanager/webhook/pod/v1/pod_handler.go index 6d5b3cc6291..cd7df17daea 100644 --- a/pkg/webhook/pod/v1/pod_handler.go +++ b/pkg/yurtmanager/webhook/pod/v1/pod_handler.go @@ -22,8 +22,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - "github.com/openyurtio/openyurt/pkg/webhook/builder" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/builder" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) const ( diff --git a/pkg/webhook/pod/v1/pod_validation.go b/pkg/yurtmanager/webhook/pod/v1/pod_validation.go similarity index 98% rename from pkg/webhook/pod/v1/pod_validation.go rename to pkg/yurtmanager/webhook/pod/v1/pod_validation.go index 410e3687688..193bf7d180f 100644 --- a/pkg/webhook/pod/v1/pod_validation.go +++ b/pkg/yurtmanager/webhook/pod/v1/pod_validation.go @@ -31,7 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/controller/yurtcoordinator/constant" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/constant" ) const ( diff --git a/pkg/webhook/server.go b/pkg/yurtmanager/webhook/server.go similarity index 76% rename from pkg/webhook/server.go rename to pkg/yurtmanager/webhook/server.go index b72f5faad57..a57ed20f149 100644 --- a/pkg/webhook/server.go +++ b/pkg/yurtmanager/webhook/server.go @@ -27,24 +27,24 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - "github.com/openyurtio/openyurt/pkg/controller/nodepool" - "github.com/openyurtio/openyurt/pkg/controller/platformadmin" - "github.com/openyurtio/openyurt/pkg/controller/raven" - ctrlutil "github.com/openyurtio/openyurt/pkg/controller/util" - "github.com/openyurtio/openyurt/pkg/controller/yurtappdaemon" - "github.com/openyurtio/openyurt/pkg/controller/yurtappset" - "github.com/openyurtio/openyurt/pkg/controller/yurtstaticset" - v1beta1gateway "github.com/openyurtio/openyurt/pkg/webhook/gateway/v1beta1" - v1node "github.com/openyurtio/openyurt/pkg/webhook/node/v1" - v1beta1nodepool "github.com/openyurtio/openyurt/pkg/webhook/nodepool/v1beta1" - v1alpha1platformadmin "github.com/openyurtio/openyurt/pkg/webhook/platformadmin/v1alpha1" - v1alpha2platformadmin "github.com/openyurtio/openyurt/pkg/webhook/platformadmin/v1alpha2" - v1pod "github.com/openyurtio/openyurt/pkg/webhook/pod/v1" - "github.com/openyurtio/openyurt/pkg/webhook/util" - webhookcontroller "github.com/openyurtio/openyurt/pkg/webhook/util/controller" - v1alpha1yurtappdaemon "github.com/openyurtio/openyurt/pkg/webhook/yurtappdaemon/v1alpha1" - v1alpha1yurtappset "github.com/openyurtio/openyurt/pkg/webhook/yurtappset/v1alpha1" - v1alpha1yurtstaticset "github.com/openyurtio/openyurt/pkg/webhook/yurtstaticset/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" + ctrlutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset" + v1beta1gateway "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/gateway/v1beta1" + v1node "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/node/v1" + v1beta1nodepool "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/nodepool/v1beta1" + v1alpha1platformadmin "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/platformadmin/v1alpha1" + v1alpha2platformadmin "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/platformadmin/v1alpha2" + v1pod "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/pod/v1" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" + webhookcontroller "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/controller" + v1alpha1yurtappdaemon "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1" + v1alpha1yurtappset "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/yurtappset/v1alpha1" + v1alpha1yurtstaticset "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/yurtstaticset/v1alpha1" ) type SetupWebhookWithManager interface { diff --git a/pkg/webhook/util/configuration/configuration.go b/pkg/yurtmanager/webhook/util/configuration/configuration.go similarity index 98% rename from pkg/webhook/util/configuration/configuration.go rename to pkg/yurtmanager/webhook/util/configuration/configuration.go index 4f1b6a20a56..9332939adfc 100644 --- a/pkg/webhook/util/configuration/configuration.go +++ b/pkg/yurtmanager/webhook/util/configuration/configuration.go @@ -28,7 +28,7 @@ import ( clientset "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" - webhookutil "github.com/openyurtio/openyurt/pkg/webhook/util" + webhookutil "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) func Ensure(kubeClient clientset.Interface, handlers map[string]struct{}, caBundle []byte, webhookPort int) error { diff --git a/pkg/webhook/util/controller/webhook_controller.go b/pkg/yurtmanager/webhook/util/controller/webhook_controller.go similarity index 97% rename from pkg/webhook/util/controller/webhook_controller.go rename to pkg/yurtmanager/webhook/util/controller/webhook_controller.go index 2734dad20e9..11eed0df2ad 100644 --- a/pkg/webhook/util/controller/webhook_controller.go +++ b/pkg/yurtmanager/webhook/util/controller/webhook_controller.go @@ -43,10 +43,10 @@ import ( "k8s.io/klog/v2" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - webhookutil "github.com/openyurtio/openyurt/pkg/webhook/util" - "github.com/openyurtio/openyurt/pkg/webhook/util/configuration" - "github.com/openyurtio/openyurt/pkg/webhook/util/generator" - "github.com/openyurtio/openyurt/pkg/webhook/util/writer" + webhookutil "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/configuration" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/generator" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/writer" ) const ( diff --git a/pkg/webhook/util/generator/certgenerator.go b/pkg/yurtmanager/webhook/util/generator/certgenerator.go similarity index 100% rename from pkg/webhook/util/generator/certgenerator.go rename to pkg/yurtmanager/webhook/util/generator/certgenerator.go diff --git a/pkg/webhook/util/generator/selfsigned.go b/pkg/yurtmanager/webhook/util/generator/selfsigned.go similarity index 100% rename from pkg/webhook/util/generator/selfsigned.go rename to pkg/yurtmanager/webhook/util/generator/selfsigned.go diff --git a/pkg/webhook/util/generator/util.go b/pkg/yurtmanager/webhook/util/generator/util.go similarity index 100% rename from pkg/webhook/util/generator/util.go rename to pkg/yurtmanager/webhook/util/generator/util.go diff --git a/pkg/webhook/util/util.go b/pkg/yurtmanager/webhook/util/util.go similarity index 100% rename from pkg/webhook/util/util.go rename to pkg/yurtmanager/webhook/util/util.go diff --git a/pkg/webhook/util/writer/atomic/atomic_writer.go b/pkg/yurtmanager/webhook/util/writer/atomic/atomic_writer.go similarity index 100% rename from pkg/webhook/util/writer/atomic/atomic_writer.go rename to pkg/yurtmanager/webhook/util/writer/atomic/atomic_writer.go diff --git a/pkg/webhook/util/writer/certwriter.go b/pkg/yurtmanager/webhook/util/writer/certwriter.go similarity index 97% rename from pkg/webhook/util/writer/certwriter.go rename to pkg/yurtmanager/webhook/util/writer/certwriter.go index da094400c71..1795ac57d0d 100644 --- a/pkg/webhook/util/writer/certwriter.go +++ b/pkg/yurtmanager/webhook/util/writer/certwriter.go @@ -22,7 +22,7 @@ import ( "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/webhook/util/generator" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/generator" ) const ( diff --git a/pkg/webhook/util/writer/error.go b/pkg/yurtmanager/webhook/util/writer/error.go similarity index 100% rename from pkg/webhook/util/writer/error.go rename to pkg/yurtmanager/webhook/util/writer/error.go diff --git a/pkg/webhook/util/writer/fs.go b/pkg/yurtmanager/webhook/util/writer/fs.go similarity index 97% rename from pkg/webhook/util/writer/fs.go rename to pkg/yurtmanager/webhook/util/writer/fs.go index 6107eb843cd..c5a9f23c5d7 100644 --- a/pkg/webhook/util/writer/fs.go +++ b/pkg/yurtmanager/webhook/util/writer/fs.go @@ -24,8 +24,8 @@ import ( "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/webhook/util/generator" - "github.com/openyurtio/openyurt/pkg/webhook/util/writer/atomic" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/generator" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/writer/atomic" ) const ( diff --git a/pkg/webhook/util/writer/secret.go b/pkg/yurtmanager/webhook/util/writer/secret.go similarity index 98% rename from pkg/webhook/util/writer/secret.go rename to pkg/yurtmanager/webhook/util/writer/secret.go index 15616351fa9..781c8801f3d 100644 --- a/pkg/webhook/util/writer/secret.go +++ b/pkg/yurtmanager/webhook/util/writer/secret.go @@ -27,7 +27,7 @@ import ( clientset "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/webhook/util/generator" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/generator" ) const ( diff --git a/pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_default.go b/pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_default.go similarity index 100% rename from pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_default.go rename to pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_default.go diff --git a/pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_handler.go b/pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_handler.go similarity index 97% rename from pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_handler.go rename to pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_handler.go index 457905fe18b..a375e9869dd 100644 --- a/pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_handler.go +++ b/pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_handler.go @@ -24,7 +24,7 @@ import ( "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) // SetupWebhookWithManager sets up Cluster webhooks. diff --git a/pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go b/pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go similarity index 100% rename from pkg/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go rename to pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1/yurtappdaemon_validation.go diff --git a/pkg/webhook/yurtappset/v1alpha1/validate.go b/pkg/yurtmanager/webhook/yurtappset/v1alpha1/validate.go similarity index 100% rename from pkg/webhook/yurtappset/v1alpha1/validate.go rename to pkg/yurtmanager/webhook/yurtappset/v1alpha1/validate.go diff --git a/pkg/webhook/yurtappset/v1alpha1/yurtappset_default.go b/pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_default.go similarity index 100% rename from pkg/webhook/yurtappset/v1alpha1/yurtappset_default.go rename to pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_default.go diff --git a/pkg/webhook/yurtappset/v1alpha1/yurtappset_handler.go b/pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_handler.go similarity index 97% rename from pkg/webhook/yurtappset/v1alpha1/yurtappset_handler.go rename to pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_handler.go index de90853e81f..9b0efc7085b 100644 --- a/pkg/webhook/yurtappset/v1alpha1/yurtappset_handler.go +++ b/pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_handler.go @@ -23,7 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) // SetupWebhookWithManager sets up Cluster webhooks. diff --git a/pkg/webhook/yurtappset/v1alpha1/yurtappset_validation.go b/pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_validation.go similarity index 100% rename from pkg/webhook/yurtappset/v1alpha1/yurtappset_validation.go rename to pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_validation.go diff --git a/pkg/webhook/yurtappset/v1alpha1/yurtappset_webhook_test.go b/pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_webhook_test.go similarity index 100% rename from pkg/webhook/yurtappset/v1alpha1/yurtappset_webhook_test.go rename to pkg/yurtmanager/webhook/yurtappset/v1alpha1/yurtappset_webhook_test.go diff --git a/pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_default.go b/pkg/yurtmanager/webhook/yurtstaticset/v1alpha1/yurtstaticset_default.go similarity index 100% rename from pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_default.go rename to pkg/yurtmanager/webhook/yurtstaticset/v1alpha1/yurtstaticset_default.go diff --git a/pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_handler.go b/pkg/yurtmanager/webhook/yurtstaticset/v1alpha1/yurtstaticset_handler.go similarity index 97% rename from pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_handler.go rename to pkg/yurtmanager/webhook/yurtstaticset/v1alpha1/yurtstaticset_handler.go index 3ad19bd456e..69a64505a48 100644 --- a/pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_handler.go +++ b/pkg/yurtmanager/webhook/yurtstaticset/v1alpha1/yurtstaticset_handler.go @@ -22,7 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" - "github.com/openyurtio/openyurt/pkg/webhook/util" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) // SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error diff --git a/pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go b/pkg/yurtmanager/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go similarity index 100% rename from pkg/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go rename to pkg/yurtmanager/webhook/yurtstaticset/v1alpha1/yurtstaticset_validation.go diff --git a/pkg/yurttunnel/util/util.go b/pkg/yurttunnel/util/util.go index f5adb8d7a03..b812c15bc66 100644 --- a/pkg/yurttunnel/util/util.go +++ b/pkg/yurttunnel/util/util.go @@ -32,8 +32,8 @@ import ( clientset "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" - "github.com/openyurtio/openyurt/pkg/profile" "github.com/openyurtio/openyurt/pkg/projectinfo" + "github.com/openyurtio/openyurt/pkg/util/profile" ) const ( From efe75d58961ee60b33e822c3fbb8786a415d7718 Mon Sep 17 00:00:00 2001 From: rambohe Date: Fri, 18 Aug 2023 09:29:23 +0800 Subject: [PATCH 79/93] improve labels and annotations definition (#1663) Signed-off-by: rambohe-ch --- .../v1alpha1/well_known_labels_annotations.go | 64 ------------------- pkg/apis/apps/v1beta1/nodepool_types.go | 2 - .../apps/well_known_labels_annotations.go | 29 ++++----- pkg/util/json.go | 50 --------------- .../filter/nodeportisolation/filter_test.go | 2 +- .../platformadmin/platformadmin_controller.go | 5 +- .../nodepool_enqueue_handlers_test.go | 11 ++-- .../controller/yurtappdaemon/revision_test.go | 3 +- .../deployment_controller.go | 13 ++-- .../deployment_controller_test.go | 3 +- .../yurtappdaemon/workloadcontroller/util.go | 4 +- .../workloadcontroller/util_test.go | 4 +- .../workloadcontroller/workload.go | 2 +- .../workloadcontroller/workload_test.go | 2 +- .../yurtappdaemon_controller_test.go | 5 +- .../yurtappset/adapter/adapter_util.go | 9 +-- .../yurtappset/adapter/adapter_util_test.go | 2 +- .../yurtappset/adapter/deployment_adapter.go | 13 ++-- .../adapter/deployment_adapter_test.go | 21 +++--- .../yurtappset/adapter/statefulset_adapter.go | 13 ++-- .../adapter/statefulset_adapter_test.go | 21 +++--- .../controller/yurtappset/pool_control.go | 3 +- .../yurtappset/yurtappset_controller_utils.go | 7 +- .../webhook/pod/v1/pod_validation.go | 6 +- test/e2e/util/nodepool.go | 6 +- 25 files changed, 95 insertions(+), 205 deletions(-) delete mode 100644 pkg/apis/apps/v1alpha1/well_known_labels_annotations.go delete mode 100644 pkg/util/json.go diff --git a/pkg/apis/apps/v1alpha1/well_known_labels_annotations.go b/pkg/apis/apps/v1alpha1/well_known_labels_annotations.go deleted file mode 100644 index da0fe354b1c..00000000000 --- a/pkg/apis/apps/v1alpha1/well_known_labels_annotations.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2021 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -@CHANGELOG -OpenYurt Authors: -change some const value -*/ - -package v1alpha1 - -// YurtAppSet & YurtAppDaemon related labels and annotations -const ( - // ControllerRevisionHashLabelKey is used to record the controller revision of current resource. - ControllerRevisionHashLabelKey = "apps.openyurt.io/controller-revision-hash" - - // PoolNameLabelKey is used to record the name of current pool. - PoolNameLabelKey = "apps.openyurt.io/pool-name" - - // SpecifiedDeleteKey indicates this object should be deleted, and the value could be the deletion option. - SpecifiedDeleteKey = "apps.openyurt.io/specified-delete" - - // AnnotationPatchKey indicates the patch for every sub pool - AnnotationPatchKey = "apps.openyurt.io/patch" - - AnnotationRefNodePool = "apps.openyurt.io/ref-nodepool" -) - -// NodePool related labels and annotations -const ( - // LabelDesiredNodePool indicates which nodepool the node want to join - LabelDesiredNodePool = "apps.openyurt.io/desired-nodepool" - - // LabelCurrentNodePool indicates which nodepool the node is currently - // belonging to - LabelCurrentNodePool = "apps.openyurt.io/nodepool" - - // LabelCurrentYurtAppDaemon indicates which service the yurtappdaemon is currently - // belonging to - LabelCurrentYurtAppDaemon = "apps.openyurt.io/yurtappdaemon" - - AnnotationPrevAttrs = "nodepool.openyurt.io/previous-attributes" - - // DefaultCloudNodePoolName defines the name of the default cloud nodepool - DefaultCloudNodePoolName = "default-nodepool" - - // DefaultEdgeNodePoolName defines the name of the default edge nodepool - DefaultEdgeNodePoolName = "default-edge-nodepool" - - // ServiceTopologyKey is the toplogy key that will be attached to node, - // the value will be the name of the nodepool - ServiceTopologyKey = "topology.kubernetes.io/zone" -) diff --git a/pkg/apis/apps/v1beta1/nodepool_types.go b/pkg/apis/apps/v1beta1/nodepool_types.go index 60ee2a08b6c..3a28b1d8f89 100644 --- a/pkg/apis/apps/v1beta1/nodepool_types.go +++ b/pkg/apis/apps/v1beta1/nodepool_types.go @@ -26,8 +26,6 @@ type NodePoolType string const ( Edge NodePoolType = "Edge" Cloud NodePoolType = "Cloud" - - NodePoolTypeLabelKey = "openyurt.io/node-pool-type" ) // NodePoolSpec defines the desired state of NodePool diff --git a/pkg/apis/apps/well_known_labels_annotations.go b/pkg/apis/apps/well_known_labels_annotations.go index 56f7abb77e4..619babbca07 100644 --- a/pkg/apis/apps/well_known_labels_annotations.go +++ b/pkg/apis/apps/well_known_labels_annotations.go @@ -1,21 +1,26 @@ /* -Copyright 2023 The OpenYurt Authors. +Copyright 2021 The OpenYurt Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +@CHANGELOG +OpenYurt Authors: +change some const value */ package apps +// YurtAppSet & YurtAppDaemon related labels and annotations const ( // ControllerRevisionHashLabelKey is used to record the controller revision of current resource. ControllerRevisionHashLabelKey = "apps.openyurt.io/controller-revision-hash" @@ -23,29 +28,17 @@ const ( // PoolNameLabelKey is used to record the name of current pool. PoolNameLabelKey = "apps.openyurt.io/pool-name" - // SpecifiedDeleteKey indicates this object should be deleted, and the value could be the deletion option. - SpecifiedDeleteKey = "apps.openyurt.io/specified-delete" - // AnnotationPatchKey indicates the patch for every sub pool AnnotationPatchKey = "apps.openyurt.io/patch" + + AnnotationRefNodePool = "apps.openyurt.io/ref-nodepool" ) // NodePool related labels and annotations const ( - NodePoolTypeLabelKey = "openyurt.io/node-pool-type" - - // LabelDesiredNodePool indicates which nodepool the node want to join - LabelDesiredNodePool = "apps.openyurt.io/desired-nodepool" - - // LabelCurrentNodePool indicates which nodepool the node is currently - // belonging to - LabelCurrentNodePool = "apps.openyurt.io/nodepool" - - AnnotationPrevAttrs = "nodepool.openyurt.io/previous-attributes" - + AnnotationPrevAttrs = "nodepool.openyurt.io/previous-attributes" NodePoolLabel = "apps.openyurt.io/nodepool" NodePoolTypeLabel = "nodepool.openyurt.io/type" NodePoolHostNetworkLabel = "nodepool.openyurt.io/hostnetwork" - - NodePoolChangedEvent = "NodePoolChanged" + NodePoolChangedEvent = "NodePoolChanged" ) diff --git a/pkg/util/json.go b/pkg/util/json.go deleted file mode 100644 index e2e91d8c69e..00000000000 --- a/pkg/util/json.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import ( - "encoding/json" - "reflect" -) - -// DumpJSON returns the JSON encoding -func DumpJSON(o interface{}) string { - j, _ := json.Marshal(o) - return string(j) -} - -// IsJSONObjectEqual checks if two objects are equal after encoding json -func IsJSONObjectEqual(o1, o2 interface{}) bool { - if reflect.DeepEqual(o1, o2) { - return true - } - - oj1, _ := json.Marshal(o1) - oj2, _ := json.Marshal(o2) - os1 := string(oj1) - os2 := string(oj2) - if os1 == os2 { - return true - } - - om1 := make(map[string]interface{}) - om2 := make(map[string]interface{}) - _ = json.Unmarshal(oj1, &om1) - _ = json.Unmarshal(oj2, &om2) - - return reflect.DeepEqual(om1, om2) -} diff --git a/pkg/yurthub/filter/nodeportisolation/filter_test.go b/pkg/yurthub/filter/nodeportisolation/filter_test.go index 8ff333256ac..c00279050b9 100644 --- a/pkg/yurthub/filter/nodeportisolation/filter_test.go +++ b/pkg/yurthub/filter/nodeportisolation/filter_test.go @@ -92,7 +92,7 @@ func TestFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "foo", Annotations: map[string]string{ - apps.LabelDesiredNodePool: nodePoolName, + apps.NodePoolLabel: nodePoolName, }, }, } diff --git a/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go index 2ee00160579..c75a161d878 100644 --- a/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go +++ b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go @@ -44,6 +44,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" iotv1alpha2 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" @@ -443,7 +444,7 @@ func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platfor } pool.NodeSelectorTerm.MatchExpressions = append(pool.NodeSelectorTerm.MatchExpressions, corev1.NodeSelectorRequirement{ - Key: appsv1alpha1.LabelCurrentNodePool, + Key: apps.NodePoolLabel, Operator: corev1.NodeSelectorOpIn, Values: []string{platformAdmin.Spec.PoolName}, }) @@ -561,7 +562,7 @@ func (r *ReconcilePlatformAdmin) handleYurtAppSet(ctx context.Context, platformA } pool.NodeSelectorTerm.MatchExpressions = append(pool.NodeSelectorTerm.MatchExpressions, corev1.NodeSelectorRequirement{ - Key: appsv1alpha1.LabelCurrentNodePool, + Key: apps.NodePoolLabel, Operator: corev1.NodeSelectorOpIn, Values: []string{platformAdmin.Spec.PoolName}, }) diff --git a/pkg/yurtmanager/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go b/pkg/yurtmanager/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go index 07c97717179..0eb99d4c9e1 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/nodepool_enqueue_handlers_test.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/event" + "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -91,7 +92,7 @@ func TestCreate(t *testing.T) { Namespace: "kube-system", Name: "kube-proxy", Annotations: map[string]string{ - appsv1alpha1.AnnotationRefNodePool: "a", + apps.AnnotationRefNodePool: "a", }, }, Spec: v1.PodSpec{}, @@ -144,7 +145,7 @@ func TestUpdate(t *testing.T) { Namespace: "kube-system", Name: "kube-proxy", Annotations: map[string]string{ - appsv1alpha1.AnnotationRefNodePool: "a", + apps.AnnotationRefNodePool: "a", }, }, Spec: v1.PodSpec{}, @@ -154,7 +155,7 @@ func TestUpdate(t *testing.T) { Namespace: "kube-system", Name: "kube-proxy", Annotations: map[string]string{ - appsv1alpha1.AnnotationRefNodePool: "a", + apps.AnnotationRefNodePool: "a", }, }, Spec: v1.PodSpec{}, @@ -206,7 +207,7 @@ func TestDelete(t *testing.T) { Namespace: "kube-system", Name: "kube-proxy", Annotations: map[string]string{ - appsv1alpha1.AnnotationRefNodePool: "a", + apps.AnnotationRefNodePool: "a", }, }, Spec: v1.PodSpec{}, @@ -260,7 +261,7 @@ func TestGeneric(t *testing.T) { Namespace: "kube-system", Name: "kube-proxy", Annotations: map[string]string{ - appsv1alpha1.AnnotationRefNodePool: "a", + apps.AnnotationRefNodePool: "a", }, }, Spec: v1.PodSpec{}, diff --git a/pkg/yurtmanager/controller/yurtappdaemon/revision_test.go b/pkg/yurtmanager/controller/yurtappdaemon/revision_test.go index 1681ec919f6..aaa0e2163ff 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/revision_test.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/revision_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client/fake" + yurtapps "github.com/openyurtio/openyurt/pkg/apis/apps" alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -81,7 +82,7 @@ func TestNewRevision(t *testing.T) { }, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - alpha1.PoolNameLabelKey: "a", + yurtapps.PoolNameLabelKey: "a", }, }, }, diff --git a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller.go index e2985991a4d..a70ad98f03b 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/openyurtio/openyurt/pkg/apis/apps" "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/refmanager" ) @@ -67,8 +68,8 @@ func (d *DeploymentControllor) applyTemplate(scheme *runtime.Scheme, yad *v1alph for k, v := range yad.Spec.Selector.MatchLabels { set.Labels[k] = v } - set.Labels[v1alpha1.ControllerRevisionHashLabelKey] = revision - set.Labels[v1alpha1.PoolNameLabelKey] = nodepool.GetName() + set.Labels[apps.ControllerRevisionHashLabelKey] = revision + set.Labels[apps.PoolNameLabelKey] = nodepool.GetName() if set.Annotations == nil { set.Annotations = map[string]string{} @@ -76,13 +77,13 @@ func (d *DeploymentControllor) applyTemplate(scheme *runtime.Scheme, yad *v1alph for k, v := range yad.Spec.WorkloadTemplate.DeploymentTemplate.Annotations { set.Annotations[k] = v } - set.Annotations[v1alpha1.AnnotationRefNodePool] = nodepool.GetName() + set.Annotations[apps.AnnotationRefNodePool] = nodepool.GetName() set.Namespace = yad.GetNamespace() set.GenerateName = getWorkloadPrefix(yad.GetName(), nodepool.GetName()) set.Spec = *yad.Spec.WorkloadTemplate.DeploymentTemplate.Spec.DeepCopy() - set.Spec.Selector.MatchLabels[v1alpha1.PoolNameLabelKey] = nodepool.GetName() + set.Spec.Selector.MatchLabels[apps.PoolNameLabelKey] = nodepool.GetName() // set RequiredDuringSchedulingIgnoredDuringExecution nil if set.Spec.Template.Spec.Affinity != nil && set.Spec.Template.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution != nil { @@ -92,8 +93,8 @@ func (d *DeploymentControllor) applyTemplate(scheme *runtime.Scheme, yad *v1alph if set.Spec.Template.Labels == nil { set.Spec.Template.Labels = map[string]string{} } - set.Spec.Template.Labels[v1alpha1.PoolNameLabelKey] = nodepool.GetName() - set.Spec.Template.Labels[v1alpha1.ControllerRevisionHashLabelKey] = revision + set.Spec.Template.Labels[apps.PoolNameLabelKey] = nodepool.GetName() + set.Spec.Template.Labels[apps.ControllerRevisionHashLabelKey] = revision // use nodeSelector set.Spec.Template.Spec.NodeSelector = CreateNodeSelectorByNodepoolName(nodepool.GetName()) diff --git a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go index af3dbd67176..6077dc82ad3 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/deployment_controller_test.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" fakeclint "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/openyurtio/openyurt/pkg/apis/apps" "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -129,7 +130,7 @@ func TestApplyTemplate(t *testing.T) { }, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - v1alpha1.PoolNameLabelKey: "a", + apps.PoolNameLabelKey: "a", }, }, }, diff --git a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util.go index e76a9d01e3d..eb0bedd6948 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util.go @@ -22,7 +22,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/validation" - "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + "github.com/openyurtio/openyurt/pkg/apis/apps" ) func getWorkloadPrefix(controllerName, nodepoolName string) string { @@ -35,7 +35,7 @@ func getWorkloadPrefix(controllerName, nodepoolName string) string { func CreateNodeSelectorByNodepoolName(nodepool string) map[string]string { return map[string]string{ - v1alpha1.LabelCurrentNodePool: nodepool, + apps.NodePoolLabel: nodepool, } } diff --git a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util_test.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util_test.go index f5591ea56ba..37ae917405d 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util_test.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/util_test.go @@ -22,7 +22,7 @@ import ( corev1 "k8s.io/api/core/v1" - "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + "github.com/openyurtio/openyurt/pkg/apis/apps" ) const ( @@ -70,7 +70,7 @@ func TestCreateNodeSelectorByNodepoolName(t *testing.T) { "normal", "a", map[string]string{ - v1alpha1.LabelCurrentNodePool: "a", + apps.NodePoolLabel: "a", }, }, } diff --git a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload.go index ff50f1f589e..0270f9f0bb0 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload.go @@ -20,7 +20,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps" ) type Workload struct { diff --git a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload_test.go b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload_test.go index 9928707e662..d6e02df6819 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload_test.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller/workload_test.go @@ -23,7 +23,7 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps" ) func TestGetRevision(t *testing.T) { diff --git a/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go index 9a668ac6963..bfdf9091125 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go @@ -25,6 +25,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/openyurtio/openyurt/pkg/apis/apps" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller" ) @@ -507,7 +508,7 @@ func TestGetTemplateControls(t *testing.T) { }, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - unitv1alpha1.PoolNameLabelKey: "a", + apps.PoolNameLabelKey: "a", }, }, }, @@ -550,7 +551,7 @@ func TestGetTemplateControls(t *testing.T) { }, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - unitv1alpha1.PoolNameLabelKey: "a", + apps.PoolNameLabelKey: "a", }, }, }, diff --git a/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util.go b/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util.go index 5808a0e9825..5b51077965f 100644 --- a/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util.go +++ b/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/klog/v2" + "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -94,7 +95,7 @@ func getRevision(objMeta metav1.Object) string { if objMeta.GetLabels() == nil { return "" } - return objMeta.GetLabels()[appsv1alpha1.ControllerRevisionHashLabelKey] + return objMeta.GetLabels()[apps.ControllerRevisionHashLabelKey] } // getCurrentPartition calculates current partition by counting the pods not having the updated revision @@ -139,7 +140,7 @@ func PoolHasPatch(poolConfig *appsv1alpha1.Pool, set metav1.Object) bool { if poolConfig.Patch == nil { // If No Patches, Must Set patches annotation to "" if anno := set.GetAnnotations(); anno != nil { - anno[appsv1alpha1.AnnotationPatchKey] = "" + anno[apps.AnnotationPatchKey] = "" } return false } @@ -154,10 +155,10 @@ func CreateNewPatchedObject(patchInfo *runtime.RawExtension, set metav1.Object, if anno := newPatched.GetAnnotations(); anno == nil { newPatched.SetAnnotations(map[string]string{ - appsv1alpha1.AnnotationPatchKey: string(patchInfo.Raw), + apps.AnnotationPatchKey: string(patchInfo.Raw), }) } else { - anno[appsv1alpha1.AnnotationPatchKey] = string(patchInfo.Raw) + anno[apps.AnnotationPatchKey] = string(patchInfo.Raw) } return nil } diff --git a/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util_test.go b/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util_test.go index 4227f4e8225..614ed00ebe3 100644 --- a/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util_test.go +++ b/pkg/yurtmanager/controller/yurtappset/adapter/adapter_util_test.go @@ -26,7 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" + unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps" ) func TestGetCurrentPartitionForStrategyOnDelete(t *testing.T) { diff --git a/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter.go b/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter.go index 143ba979c03..72b7971b6cb 100644 --- a/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter.go +++ b/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/openyurtio/openyurt/pkg/apis/apps" alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -100,9 +101,9 @@ func (a *DeploymentAdapter) ApplyPoolTemplate(yas *alpha1.YurtAppSet, poolName, for k, v := range yas.Spec.Selector.MatchLabels { set.Labels[k] = v } - set.Labels[alpha1.ControllerRevisionHashLabelKey] = revision + set.Labels[apps.ControllerRevisionHashLabelKey] = revision // record the pool name as a label - set.Labels[alpha1.PoolNameLabelKey] = poolName + set.Labels[apps.PoolNameLabelKey] = poolName if set.Annotations == nil { set.Annotations = map[string]string{} @@ -114,7 +115,7 @@ func (a *DeploymentAdapter) ApplyPoolTemplate(yas *alpha1.YurtAppSet, poolName, set.GenerateName = getPoolPrefix(yas.Name, poolName) selectors := yas.Spec.Selector.DeepCopy() - selectors.MatchLabels[alpha1.PoolNameLabelKey] = poolName + selectors.MatchLabels[apps.PoolNameLabelKey] = poolName if err := controllerutil.SetControllerReference(yas, set, a.Scheme); err != nil { return err @@ -128,8 +129,8 @@ func (a *DeploymentAdapter) ApplyPoolTemplate(yas *alpha1.YurtAppSet, poolName, if set.Spec.Template.Labels == nil { set.Spec.Template.Labels = map[string]string{} } - set.Spec.Template.Labels[alpha1.PoolNameLabelKey] = poolName - set.Spec.Template.Labels[alpha1.ControllerRevisionHashLabelKey] = revision + set.Spec.Template.Labels[apps.PoolNameLabelKey] = poolName + set.Spec.Template.Labels[apps.ControllerRevisionHashLabelKey] = revision set.Spec.RevisionHistoryLimit = yas.Spec.RevisionHistoryLimit set.Spec.MinReadySeconds = yas.Spec.WorkloadTemplate.DeploymentTemplate.Spec.MinReadySeconds @@ -166,5 +167,5 @@ func (a *DeploymentAdapter) PostUpdate(yas *alpha1.YurtAppSet, obj runtime.Objec // IsExpected checks the pool is the expected revision or not. // The revision label can tell the current pool revision. func (a *DeploymentAdapter) IsExpected(obj metav1.Object, revision string) bool { - return obj.GetLabels()[alpha1.ControllerRevisionHashLabelKey] != revision + return obj.GetLabels()[apps.ControllerRevisionHashLabelKey] != revision } diff --git a/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter_test.go b/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter_test.go index b61c0af0f9e..c0f41234cc7 100644 --- a/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter_test.go +++ b/pkg/yurtmanager/controller/yurtappset/adapter/deployment_adapter_test.go @@ -27,6 +27,7 @@ import ( fakeclint "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -59,7 +60,7 @@ func TestDeploymentAdapter_ApplyPoolTemplate(t *testing.T) { DeploymentTemplate: &appsv1alpha1.DeploymentTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - appsv1alpha1.AnnotationPatchKey: "annotation-v", + apps.AnnotationPatchKey: "annotation-v", }, Labels: map[string]string{ "name": "foo", @@ -113,29 +114,29 @@ func TestDeploymentAdapter_ApplyPoolTemplate(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Labels: map[string]string{ - "name": "foo", - appsv1alpha1.ControllerRevisionHashLabelKey: "1", - appsv1alpha1.PoolNameLabelKey: "hangzhou", + "name": "foo", + apps.ControllerRevisionHashLabelKey: "1", + apps.PoolNameLabelKey: "hangzhou", }, Annotations: map[string]string{ - appsv1alpha1.AnnotationPatchKey: "", + apps.AnnotationPatchKey: "", }, GenerateName: "foo-hangzhou-", }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - "name": "foo", - appsv1alpha1.PoolNameLabelKey: "hangzhou", + "name": "foo", + apps.PoolNameLabelKey: "hangzhou", }, }, Replicas: &one, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ - "name": "foo", - appsv1alpha1.ControllerRevisionHashLabelKey: "1", - appsv1alpha1.PoolNameLabelKey: "hangzhou", + "name": "foo", + apps.ControllerRevisionHashLabelKey: "1", + apps.PoolNameLabelKey: "hangzhou", }, }, Spec: corev1.PodSpec{ diff --git a/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter.go b/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter.go index d9baf2f6566..be8a2710d4c 100644 --- a/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter.go +++ b/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/openyurtio/openyurt/pkg/apis/apps" alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -106,9 +107,9 @@ func (a *StatefulSetAdapter) ApplyPoolTemplate(yas *alpha1.YurtAppSet, poolName, for k, v := range yas.Spec.Selector.MatchLabels { set.Labels[k] = v } - set.Labels[alpha1.ControllerRevisionHashLabelKey] = revision + set.Labels[apps.ControllerRevisionHashLabelKey] = revision // record the pool name as a label - set.Labels[alpha1.PoolNameLabelKey] = poolName + set.Labels[apps.PoolNameLabelKey] = poolName if set.Annotations == nil { set.Annotations = map[string]string{} @@ -120,7 +121,7 @@ func (a *StatefulSetAdapter) ApplyPoolTemplate(yas *alpha1.YurtAppSet, poolName, set.GenerateName = getPoolPrefix(yas.Name, poolName) selectors := yas.Spec.Selector.DeepCopy() - selectors.MatchLabels[alpha1.PoolNameLabelKey] = poolName + selectors.MatchLabels[apps.PoolNameLabelKey] = poolName if err := controllerutil.SetControllerReference(yas, set, a.Scheme); err != nil { return err @@ -134,8 +135,8 @@ func (a *StatefulSetAdapter) ApplyPoolTemplate(yas *alpha1.YurtAppSet, poolName, if set.Spec.Template.Labels == nil { set.Spec.Template.Labels = map[string]string{} } - set.Spec.Template.Labels[alpha1.PoolNameLabelKey] = poolName - set.Spec.Template.Labels[alpha1.ControllerRevisionHashLabelKey] = revision + set.Spec.Template.Labels[apps.PoolNameLabelKey] = poolName + set.Spec.Template.Labels[apps.ControllerRevisionHashLabelKey] = revision set.Spec.RevisionHistoryLimit = yas.Spec.RevisionHistoryLimit set.Spec.PodManagementPolicy = yas.Spec.WorkloadTemplate.StatefulSetTemplate.Spec.PodManagementPolicy @@ -182,7 +183,7 @@ func (a *StatefulSetAdapter) PostUpdate(yas *alpha1.YurtAppSet, obj runtime.Obje // IsExpected checks the pool is the expected revision or not. // The revision label can tell the current pool revision. func (a *StatefulSetAdapter) IsExpected(obj metav1.Object, revision string) bool { - return obj.GetLabels()[alpha1.ControllerRevisionHashLabelKey] != revision + return obj.GetLabels()[apps.ControllerRevisionHashLabelKey] != revision } /* diff --git a/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter_test.go b/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter_test.go index de296193fa9..2eb70775a33 100644 --- a/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter_test.go +++ b/pkg/yurtmanager/controller/yurtappset/adapter/statefulset_adapter_test.go @@ -27,6 +27,7 @@ import ( fakeclint "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -58,7 +59,7 @@ func TestStatefulSetAdapter_ApplyPoolTemplate(t *testing.T) { StatefulSetTemplate: &appsv1alpha1.StatefulSetTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - appsv1alpha1.AnnotationPatchKey: "annotation-v", + apps.AnnotationPatchKey: "annotation-v", }, Labels: map[string]string{ "name": "foo", @@ -112,29 +113,29 @@ func TestStatefulSetAdapter_ApplyPoolTemplate(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Labels: map[string]string{ - "name": "foo", - appsv1alpha1.ControllerRevisionHashLabelKey: "1", - appsv1alpha1.PoolNameLabelKey: "hangzhou", + "name": "foo", + apps.ControllerRevisionHashLabelKey: "1", + apps.PoolNameLabelKey: "hangzhou", }, Annotations: map[string]string{ - appsv1alpha1.AnnotationPatchKey: "", + apps.AnnotationPatchKey: "", }, GenerateName: "foo-hangzhou-", }, Spec: appsv1.StatefulSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - "name": "foo", - appsv1alpha1.PoolNameLabelKey: "hangzhou", + "name": "foo", + apps.PoolNameLabelKey: "hangzhou", }, }, Replicas: &one, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ - "name": "foo", - appsv1alpha1.ControllerRevisionHashLabelKey: "1", - appsv1alpha1.PoolNameLabelKey: "hangzhou", + "name": "foo", + apps.ControllerRevisionHashLabelKey: "1", + apps.PoolNameLabelKey: "hangzhou", }, }, Spec: corev1.PodSpec{ diff --git a/pkg/yurtmanager/controller/yurtappset/pool_control.go b/pkg/yurtmanager/controller/yurtappset/pool_control.go index 34791df33bf..45f07095fb7 100644 --- a/pkg/yurtmanager/controller/yurtappset/pool_control.go +++ b/pkg/yurtmanager/controller/yurtappset/pool_control.go @@ -28,6 +28,7 @@ import ( "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/openyurtio/openyurt/pkg/apis/apps" alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/refmanager" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/adapter" @@ -168,7 +169,7 @@ func (m *PoolControl) convertToPool(set metav1.Object) (*Pool, error) { ReplicasInfo: specReplicas, }, } - if data, ok := set.GetAnnotations()[alpha1.AnnotationPatchKey]; ok { + if data, ok := set.GetAnnotations()[apps.AnnotationPatchKey]; ok { pool.Status.PatchInfo = data } return pool, nil diff --git a/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_utils.go b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_utils.go index b96bbffddf4..3c7d22a560c 100644 --- a/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_utils.go +++ b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller_utils.go @@ -28,6 +28,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/openyurtio/openyurt/pkg/apis/apps" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" ) @@ -39,13 +40,13 @@ type YurtAppSetPatches struct { } func getPoolNameFrom(metaObj metav1.Object) (string, error) { - name, exist := metaObj.GetLabels()[unitv1alpha1.PoolNameLabelKey] + name, exist := metaObj.GetLabels()[apps.PoolNameLabelKey] if !exist { - return "", fmt.Errorf("fail to get pool name from label of pool %s/%s: no label %s found", metaObj.GetNamespace(), metaObj.GetName(), unitv1alpha1.PoolNameLabelKey) + return "", fmt.Errorf("fail to get pool name from label of pool %s/%s: no label %s found", metaObj.GetNamespace(), metaObj.GetName(), apps.PoolNameLabelKey) } if len(name) == 0 { - return "", fmt.Errorf("fail to get pool name from label of pool %s/%s: label %s has an empty value", metaObj.GetNamespace(), metaObj.GetName(), unitv1alpha1.PoolNameLabelKey) + return "", fmt.Errorf("fail to get pool name from label of pool %s/%s: label %s has an empty value", metaObj.GetNamespace(), metaObj.GetName(), apps.PoolNameLabelKey) } return name, nil diff --git a/pkg/yurtmanager/webhook/pod/v1/pod_validation.go b/pkg/yurtmanager/webhook/pod/v1/pod_validation.go index 193bf7d180f..249429d12e5 100644 --- a/pkg/yurtmanager/webhook/pod/v1/pod_validation.go +++ b/pkg/yurtmanager/webhook/pod/v1/pod_validation.go @@ -30,13 +30,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/constant" ) const ( - LabelCurrentNodePool = "apps.openyurt.io/nodepool" - UserNodeController = "system:serviceaccount:kube-system:node-controller" + UserNodeController = "system:serviceaccount:kube-system:node-controller" NodeLeaseDurationSeconds = 40 DefaultPoolReadyNodeNumberRatioThreshold = 0.35 @@ -83,7 +83,7 @@ func validatePodDeletion(cli client.Client, pod *v1.Pod, req admission.Request) // only validate pod which in nodePool var nodePoolName string if node.Labels != nil { - if name, ok := node.Labels[LabelCurrentNodePool]; ok { + if name, ok := node.Labels[apps.NodePoolLabel]; ok { nodePoolName = name } } diff --git a/test/e2e/util/nodepool.go b/test/e2e/util/nodepool.go index 7afd6cb3173..3c3bfd1c789 100644 --- a/test/e2e/util/nodepool.go +++ b/test/e2e/util/nodepool.go @@ -52,8 +52,8 @@ func CleanupNodePoolLabel(ctx context.Context, k8sClient client.Client) error { newNode := originNode.DeepCopy() if newNode.Labels != nil { for k := range newNode.Labels { - if k == apps.LabelDesiredNodePool { - delete(newNode.Labels, apps.LabelDesiredNodePool) + if k == apps.NodePoolLabel { + delete(newNode.Labels, apps.NodePoolLabel) labelDeleted = true } } @@ -103,7 +103,7 @@ func InitNodeAndNodePool(ctx context.Context, k8sClient client.Client, poolToNod continue } - nodeLabels[apps.LabelDesiredNodePool] = nodeToPoolMap[originNode.Name] + nodeLabels[apps.NodePoolLabel] = nodeToPoolMap[originNode.Name] newNode.Labels = nodeLabels if err := k8sClient.Patch(ctx, newNode, client.MergeFrom(&originNode)); err != nil { return err From d15078f7bc19857e87952ba418fb1dd71d191bfa Mon Sep 17 00:00:00 2001 From: Zhen Zhao <413621396@qq.com> Date: Fri, 18 Aug 2023 11:05:24 +0800 Subject: [PATCH 80/93] fix node-servant convert not use yss template to deploy yurthub (#1633) * fix node-servant convert not use yss template to deploy yurthub * update --- pkg/node-servant/components/yurthub.go | 37 ++++--- pkg/node-servant/constant.go | 6 + test/e2e/cmd/init/constants/constants.go | 133 +++++++++++++++++++++++ test/e2e/cmd/init/converter.go | 43 +++++++- 4 files changed, 199 insertions(+), 20 deletions(-) diff --git a/pkg/node-servant/components/yurthub.go b/pkg/node-servant/components/yurthub.go index 94ff7957b59..ee20b5ed12e 100644 --- a/pkg/node-servant/components/yurthub.go +++ b/pkg/node-servant/components/yurthub.go @@ -23,7 +23,6 @@ import ( "net/url" "os" "path/filepath" - "strconv" "strings" "time" @@ -34,7 +33,7 @@ import ( "github.com/openyurtio/openyurt/pkg/projectinfo" kubeconfigutil "github.com/openyurtio/openyurt/pkg/util/kubeconfig" - "github.com/openyurtio/openyurt/pkg/util/templates" + tmplutil "github.com/openyurtio/openyurt/pkg/util/templates" "github.com/openyurtio/openyurt/pkg/yurtadm/constants" enutil "github.com/openyurtio/openyurt/pkg/yurtadm/util/edgenode" "github.com/openyurtio/openyurt/pkg/yurthub/storage/disk" @@ -46,6 +45,8 @@ const ( fileMode = 0666 DefaultRootDir = "/var/lib" DefaultCaPath = "/etc/kubernetes/pki/ca.crt" + yurthubYurtStaticSetName = "yurthub" + defaultConfigmapPath = "/data" ) type yurtHubOperator struct { @@ -77,20 +78,8 @@ func (op *yurtHubOperator) Install() error { // 1. put yurt-hub yaml into /etc/kubernetes/manifests klog.Infof("setting up yurthub on node") - // 1-1. replace variables in yaml file - klog.Infof("setting up yurthub apiServer addr") - yurthubTemplate, err := templates.SubsituteTemplate(constants.YurthubTemplate, map[string]string{ - "yurthubBindingAddr": constants.DefaultYurtHubServerAddr, - "kubernetesServerAddr": op.apiServerAddr, - "image": op.yurthubImage, - "bootstrapFile": constants.YurtHubBootstrapConfig, - "workingMode": string(op.workingMode), - "enableDummyIf": strconv.FormatBool(op.enableDummyIf), - "enableNodePool": strconv.FormatBool(op.enableNodePool), - }) - if err != nil { - return err - } + // 1-1. get configmap data path + configMapDataPath := filepath.Join(defaultConfigmapPath, yurthubYurtStaticSetName) // 1-2. create /var/lib/yurthub/bootstrap-hub.conf if err := enutil.EnsureDir(constants.YurtHubWorkdir); err != nil { @@ -106,10 +95,24 @@ func (op *yurtHubOperator) Install() error { if err := enutil.EnsureDir(podManifestPath); err != nil { return err } - if err := os.WriteFile(getYurthubYaml(podManifestPath), []byte(yurthubTemplate), fileMode); err != nil { + content, err := os.ReadFile(configMapDataPath) + if err != nil { + return fmt.Errorf("failed to read source file %s: %w", configMapDataPath, err) + } + klog.Infof("yurt-hub.yaml apiServerAddr: %+v", op.apiServerAddr) + yssYurtHub, err := tmplutil.SubsituteTemplate(string(content), map[string]string{ + "kubernetesServerAddr": op.apiServerAddr, + }) + if err != nil { return err } + if err = os.WriteFile(getYurthubYaml(podManifestPath), []byte(yssYurtHub), fileMode); err != nil { + return err + } + klog.Infof("create the %s/yurt-hub.yaml", podManifestPath) + klog.Infof("yurt-hub.yaml: %+v", configMapDataPath) + klog.Infof("yurt-hub.yaml content: %+v", yssYurtHub) // 2. wait yurthub pod to be ready return hubHealthcheck(op.yurthubHealthCheckTimeout) diff --git a/pkg/node-servant/constant.go b/pkg/node-servant/constant.go index b91d0b4ed0b..f71cf62dd92 100644 --- a/pkg/node-servant/constant.go +++ b/pkg/node-servant/constant.go @@ -42,6 +42,10 @@ spec: hostPath: path: / type: Directory + - name: configmap + configMap: + defaultMode: 420 + name: {{.configmap_name}} containers: - name: node-servant-servant image: {{.node_servant_image}} @@ -56,6 +60,8 @@ spec: volumeMounts: - mountPath: /openyurt name: host-root + - mountPath: /openyurt/data + name: configmap env: - name: NODE_NAME valueFrom: diff --git a/test/e2e/cmd/init/constants/constants.go b/test/e2e/cmd/init/constants/constants.go index 5d4300a9876..228bc16efad 100644 --- a/test/e2e/cmd/init/constants/constants.go +++ b/test/e2e/cmd/init/constants/constants.go @@ -187,4 +187,137 @@ data: discardcloudservice: "" masterservice: "" ` + + YurthubCloudYurtStaticSet = ` +apiVersion: apps.openyurt.io/v1alpha1 +kind: YurtStaticSet +metadata: + name: yurt-hub-cloud + namespace: "kube-system" +spec: + staticPodManifest: yurthub + template: + metadata: + labels: + k8s-app: yurt-hub-cloud + spec: + volumes: + - name: hub-dir + hostPath: + path: /var/lib/yurthub + type: DirectoryOrCreate + - name: kubernetes + hostPath: + path: /etc/kubernetes + type: Directory + containers: + - name: yurt-hub + image: {{.yurthub_image}} + imagePullPolicy: IfNotPresent + volumeMounts: + - name: hub-dir + mountPath: /var/lib/yurthub + - name: kubernetes + mountPath: /etc/kubernetes + command: + - yurthub + - --v=2 + - --bind-address=127.0.0.1 + - --server-addr={{.kubernetesServerAddr}} + - --node-name=$(NODE_NAME) + - --bootstrap-file=/var/lib/yurthub/bootstrap-hub.conf + - --working-mode=cloud + - --namespace="kube-system" + livenessProbe: + httpGet: + host: 127.0.0.1 + path: /v1/healthz + port: 10267 + initialDelaySeconds: 300 + periodSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 150m + memory: 150Mi + limits: + memory: 300Mi + securityContext: + capabilities: + add: [ "NET_ADMIN", "NET_RAW" ] + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + hostNetwork: true + priorityClassName: system-node-critical + priority: 2000001000 +` + YurthubYurtStaticSet = ` +apiVersion: apps.openyurt.io/v1alpha1 +kind: YurtStaticSet +metadata: + name: yurt-hub + namespace: "kube-system" +spec: + staticPodManifest: yurthub + template: + metadata: + labels: + k8s-app: yurt-hub + spec: + volumes: + - name: hub-dir + hostPath: + path: /var/lib/yurthub + type: DirectoryOrCreate + - name: kubernetes + hostPath: + path: /etc/kubernetes + type: Directory + containers: + - name: yurt-hub + image: {{.yurthub_image}} + imagePullPolicy: IfNotPresent + volumeMounts: + - name: hub-dir + mountPath: /var/lib/yurthub + - name: kubernetes + mountPath: /etc/kubernetes + command: + - yurthub + - --v=2 + - --bind-address=127.0.0.1 + - --server-addr={{.kubernetesServerAddr}} + - --node-name=$(NODE_NAME) + - --bootstrap-file=/var/lib/yurthub/bootstrap-hub.conf + - --working-mode=edge + - --namespace="kube-system" + livenessProbe: + httpGet: + host: 127.0.0.1 + path: /v1/healthz + port: 10267 + initialDelaySeconds: 300 + periodSeconds: 5 + failureThreshold: 3 + resources: + requests: + cpu: 150m + memory: 150Mi + limits: + memory: 300Mi + securityContext: + capabilities: + add: [ "NET_ADMIN", "NET_RAW" ] + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + hostNetwork: true + priorityClassName: system-node-critical + priority: 2000001000 +` ) diff --git a/test/e2e/cmd/init/converter.go b/test/e2e/cmd/init/converter.go index 9108248b90e..65adca57541 100644 --- a/test/e2e/cmd/init/converter.go +++ b/test/e2e/cmd/init/converter.go @@ -39,7 +39,9 @@ import ( nodeservant "github.com/openyurtio/openyurt/pkg/node-servant" kubeadmapi "github.com/openyurtio/openyurt/pkg/util/kubernetes/kubeadm/app/phases/bootstraptoken/clusterinfo" strutil "github.com/openyurtio/openyurt/pkg/util/strings" + tmplutil "github.com/openyurtio/openyurt/pkg/util/templates" "github.com/openyurtio/openyurt/pkg/yurthub/util" + "github.com/openyurtio/openyurt/test/e2e/cmd/init/constants" "github.com/openyurtio/openyurt/test/e2e/cmd/init/lock" kubeutil "github.com/openyurtio/openyurt/test/e2e/cmd/init/util/kubernetes" ) @@ -47,6 +49,8 @@ import ( const ( // defaultYurthubHealthCheckTimeout defines the default timeout for yurthub health check phase defaultYurthubHealthCheckTimeout = 2 * time.Minute + yssYurtHubCloudName = "yurt-static-set-yurt-hub-cloud" + yssYurtHubName = "yurt-static-set-yurt-hub" ) type ClusterConverter struct { @@ -124,14 +128,45 @@ func (c *ClusterConverter) deployYurthub() error { // The node-servant will detect the kubeadm_conf_path automatically // It will be either "/usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf" // or "/etc/systemd/system/kubelet.service.d/10-kubeadm.conf". - "kubeadm_conf_path": "", - "working_mode": string(util.WorkingModeEdge), - "enable_dummy_if": strconv.FormatBool(c.EnableDummyIf), + "kubeadm_conf_path": "", + "working_mode": string(util.WorkingModeEdge), + "enable_dummy_if": strconv.FormatBool(c.EnableDummyIf), + "kubernetesServerAddr": "{{.kubernetesServerAddr}}", } if c.YurthubHealthCheckTimeout != defaultYurthubHealthCheckTimeout { convertCtx["yurthub_healthcheck_timeout"] = c.YurthubHealthCheckTimeout.String() } + // create the yurthub-cloud and yurthub yss + tempDir, err := os.MkdirTemp(c.RootDir, "yurt-hub") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + tempFile := filepath.Join(tempDir, "yurthub-cloud-yurtstaticset.yaml") + yssYurtHubCloud, err := tmplutil.SubsituteTemplate(constants.YurthubCloudYurtStaticSet, convertCtx) + if err != nil { + return err + } + if err = os.WriteFile(tempFile, []byte(yssYurtHubCloud), 0644); err != nil { + return err + } + if err = c.ComponentsBuilder.InstallComponents(tempFile, false); err != nil { + return err + } + + tempFile = filepath.Join(tempDir, "yurthub-yurtstaticset.yaml") + yssYurtHub, err := tmplutil.SubsituteTemplate(constants.YurthubYurtStaticSet, convertCtx) + if err != nil { + return err + } + if err = os.WriteFile(tempFile, []byte(yssYurtHub), 0644); err != nil { + return err + } + if err = c.ComponentsBuilder.InstallComponents(tempFile, false); err != nil { + return err + } + npExist, err := nodePoolResourceExists(c.ClientSet) if err != nil { return err @@ -141,6 +176,7 @@ func (c *ClusterConverter) deployYurthub() error { if len(c.EdgeNodes) != 0 { convertCtx["working_mode"] = string(util.WorkingModeEdge) + convertCtx["configmap_name"] = yssYurtHubName if err = kubeutil.RunServantJobs(c.ClientSet, c.WaitServantJobTimeout, func(nodeName string) (*batchv1.Job, error) { return nodeservant.RenderNodeServantJob("convert", convertCtx, nodeName) }, c.EdgeNodes, os.Stderr, false); err != nil { @@ -175,6 +211,7 @@ func (c *ClusterConverter) deployYurthub() error { // deploy yurt-hub and reset the kubelet service on cloud nodes convertCtx["working_mode"] = string(util.WorkingModeCloud) + convertCtx["configmap_name"] = yssYurtHubCloudName klog.Infof("convert context for cloud nodes(%q): %#+v", c.CloudNodes, convertCtx) if err = kubeutil.RunServantJobs(c.ClientSet, c.WaitServantJobTimeout, func(nodeName string) (*batchv1.Job, error) { return nodeservant.RenderNodeServantJob("convert", convertCtx, nodeName) From b6f2ec0b2d81c1d82aeb06b35b2f5a172f66df18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=91=B8=E9=B1=BC=E5=96=B5?= Date: Mon, 21 Aug 2023 17:09:24 +0800 Subject: [PATCH 81/93] feat: establish the component mechanism of the iot system (#1650) * feat: extend manifest to support the component mechanism Signed-off-by: LavenderQAQ * feat: read the manifest file when the yurt-manager is started Signed-off-by: LavenderQAQ * feat: modified reconciler to support dynamic component mechanism Signed-off-by: LavenderQAQ * fix: add common-config-bootstrapper component in minnesota version Signed-off-by: LavenderQAQ * rebase: move code to new yurtmanager location Signed-off-by: LavenderQAQ --------- Signed-off-by: LavenderQAQ --- .../crds/iot.openyurt.io_platformadmins.yaml | 2 - pkg/apis/iot/v1alpha2/platformadmin_types.go | 3 - .../config/EdgeXConfig/manifest.yaml | 43 ++++- .../config/{types.go => config.go} | 65 ++++++-- .../platformadmin/{util.go => iotdock.go} | 4 +- .../platformadmin/platformadmin_controller.go | 151 +++++++++++++++--- .../v1alpha1/platformadmin_handler.go | 17 +- .../v1alpha2/platformadmin_handler.go | 17 +- .../v1alpha2/platformadmin_validation.go | 5 +- 9 files changed, 235 insertions(+), 72 deletions(-) rename pkg/yurtmanager/controller/platformadmin/config/{types.go => config.go} (70%) rename pkg/yurtmanager/controller/platformadmin/{util.go => iotdock.go} (96%) diff --git a/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml b/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml index 9b71d04091b..edb4df7fd76 100644 --- a/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml +++ b/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml @@ -8421,8 +8421,6 @@ spec: items: description: Component defines the components of EdgeX properties: - image: - type: string name: type: string required: diff --git a/pkg/apis/iot/v1alpha2/platformadmin_types.go b/pkg/apis/iot/v1alpha2/platformadmin_types.go index 92114e58cde..a9be10a55aa 100644 --- a/pkg/apis/iot/v1alpha2/platformadmin_types.go +++ b/pkg/apis/iot/v1alpha2/platformadmin_types.go @@ -40,9 +40,6 @@ type PlatformAdminConditionSeverity string // Component defines the components of EdgeX type Component struct { Name string `json:"name"` - - // +optional - Image string `json:"image,omitempty"` } // PlatformAdminSpec defines the desired state of PlatformAdmin diff --git a/pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/manifest.yaml b/pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/manifest.yaml index a5f1096515f..cf5bf7c3acb 100644 --- a/pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/manifest.yaml +++ b/pkg/yurtmanager/controller/platformadmin/config/EdgeXConfig/manifest.yaml @@ -2,9 +2,40 @@ updated: false count: 6 latestVersion: minnesota versions: -- kamakura -- jakarta -- levski -- minnesota -- ireland -- hanoi +- name: kamakura + requiredComponents: + - edgex-core-command + - edgex-core-consul + - edgex-core-metadata + - edgex-redis +- name: jakarta + requiredComponents: + - edgex-core-command + - edgex-core-consul + - edgex-core-metadata + - edgex-redis +- name: levski + requiredComponents: + - edgex-core-command + - edgex-core-consul + - edgex-core-metadata + - edgex-redis +- name: minnesota + requiredComponents: + - edgex-core-command + - edgex-core-consul + - edgex-core-metadata + - edgex-redis + - edgex-core-common-config-bootstrapper +- name: ireland + requiredComponents: + - edgex-core-command + - edgex-core-consul + - edgex-core-metadata + - edgex-redis +- name: hanoi + requiredComponents: + - edgex-core-command + - edgex-core-consul + - edgex-core-metadata + - edgex-redis diff --git a/pkg/yurtmanager/controller/platformadmin/config/types.go b/pkg/yurtmanager/controller/platformadmin/config/config.go similarity index 70% rename from pkg/yurtmanager/controller/platformadmin/config/types.go rename to pkg/yurtmanager/controller/platformadmin/config/config.go index e3727da1a9c..fa0cf36fbec 100644 --- a/pkg/yurtmanager/controller/platformadmin/config/types.go +++ b/pkg/yurtmanager/controller/platformadmin/config/config.go @@ -21,11 +21,22 @@ import ( "encoding/json" "path/filepath" + "gopkg.in/yaml.v3" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" ) +var ( + //go:embed EdgeXConfig + EdgeXFS embed.FS + folder = "EdgeXConfig/" + ManifestPath = filepath.Join(folder, "manifest.yaml") + securityFile = filepath.Join(folder, "config.json") + nosectyFile = filepath.Join(folder, "config-nosecty.json") +) + type EdgeXConfig struct { Versions []*Version `yaml:"versions,omitempty" json:"versions,omitempty"` } @@ -42,20 +53,42 @@ type Component struct { Deployment *appsv1.DeploymentSpec `yaml:"deployment,omitempty" json:"deployment,omitempty"` } -var ( - //go:embed EdgeXConfig - EdgeXFS embed.FS - ManifestPath = filepath.Join(folder, "manifest.yaml") -) +type Manifest struct { + Updated string `yaml:"updated"` + Count int `yaml:"count"` + LatestVersion string `yaml:"latestVersion"` + Versions []ManifestVersion `yaml:"versions"` +} -var ( - folder = "EdgeXConfig/" - securityFile = filepath.Join(folder, "config.json") - nosectyFile = filepath.Join(folder, "config-nosecty.json") -) +type ManifestVersion struct { + Name string `yaml:"name"` + RequiredComponents []string `yaml:"requiredComponents"` +} + +func ExtractVersionsName(manifest *Manifest) sets.String { + versionsNameSet := sets.NewString() + for _, version := range manifest.Versions { + versionsNameSet.Insert(version.Name) + } + return versionsNameSet +} + +func ExtractRequiredComponentsName(manifest *Manifest, versionName string) sets.String { + requiredComponentSet := sets.NewString() + for _, version := range manifest.Versions { + if version.Name == versionName { + for _, c := range version.RequiredComponents { + requiredComponentSet.Insert(c) + } + break + } + } + return requiredComponentSet +} // PlatformAdminControllerConfiguration contains elements describing PlatformAdminController. type PlatformAdminControllerConfiguration struct { + Manifest Manifest SecurityComponents map[string][]*Component NoSectyComponents map[string][]*Component SecurityConfigMaps map[string][]corev1.ConfigMap @@ -67,6 +100,7 @@ func NewPlatformAdminControllerConfiguration() *PlatformAdminControllerConfigura edgexconfig = EdgeXConfig{} edgexnosectyconfig = EdgeXConfig{} conf = PlatformAdminControllerConfiguration{ + Manifest: Manifest{}, SecurityComponents: make(map[string][]*Component), NoSectyComponents: make(map[string][]*Component), SecurityConfigMaps: make(map[string][]corev1.ConfigMap), @@ -74,6 +108,12 @@ func NewPlatformAdminControllerConfiguration() *PlatformAdminControllerConfigura } ) + // Read the EdgeX configuration file + manifestContent, err := EdgeXFS.ReadFile(ManifestPath) + if err != nil { + klog.Errorf("File to open the embed EdgeX manifest file: %v", err) + return nil + } securityContent, err := EdgeXFS.ReadFile(securityFile) if err != nil { klog.Errorf("Fail to open the embed EdgeX security config: %v", err) @@ -85,6 +125,11 @@ func NewPlatformAdminControllerConfiguration() *PlatformAdminControllerConfigura return nil } + // Unmarshal the EdgeX configuration file + if err := yaml.Unmarshal(manifestContent, &conf.Manifest); err != nil { + klog.Errorf("Error manifest EdgeX configuration file: %v", err) + return nil + } if err = json.Unmarshal(securityContent, &edgexconfig); err != nil { klog.Errorf("Fail to unmarshal the embed EdgeX security config: %v", err) return nil diff --git a/pkg/yurtmanager/controller/platformadmin/util.go b/pkg/yurtmanager/controller/platformadmin/iotdock.go similarity index 96% rename from pkg/yurtmanager/controller/platformadmin/util.go rename to pkg/yurtmanager/controller/platformadmin/iotdock.go index 9975c683083..a1f98c1cde1 100644 --- a/pkg/yurtmanager/controller/platformadmin/util.go +++ b/pkg/yurtmanager/controller/platformadmin/iotdock.go @@ -31,8 +31,8 @@ import ( utils "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/utils" ) -// NewYurtIoTDockComponent initialize the configuration of yurt-iot-dock component -func NewYurtIoTDockComponent(platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminFramework *PlatformAdminFramework) (*config.Component, error) { +// newYurtIoTDockComponent initialize the configuration of yurt-iot-dock component +func newYurtIoTDockComponent(platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminFramework *PlatformAdminFramework) (*config.Component, error) { var yurtIotDockComponent config.Component // If the configuration of the yurt-iot-dock component that customized in the platformAdminFramework diff --git a/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go index c75a161d878..46686d11dab 100644 --- a/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go +++ b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go @@ -31,6 +31,7 @@ import ( kjson "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" "k8s.io/kubectl/pkg/scheme" @@ -244,7 +245,7 @@ func (r *ReconcilePlatformAdmin) reconcileDelete(ctx context.Context, platformAd klog.V(4).Infof(Format("ReconcileDelete PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) yas := &appsv1alpha1.YurtAppSet{} - platformAdminFramework, err := r.syncFramework(ctx, platformAdmin) + platformAdminFramework, err := r.readFramework(ctx, platformAdmin) if err != nil { return reconcile.Result{}, errors.Wrapf(err, "unexpected error while synchronizing customize framework for %s", platformAdmin.Namespace+"/"+platformAdmin.Name) } @@ -300,7 +301,7 @@ func (r *ReconcilePlatformAdmin) reconcileNormal(ctx context.Context, platformAd // Note that this configmap is different from the one below, which is used to customize the edgex framework // Sync configmap of edgex confiruation during initialization // This framework pointer is needed to synchronize user-modified edgex configurations - platformAdminFramework, err := r.syncFramework(ctx, platformAdmin) + platformAdminFramework, err := r.readFramework(ctx, platformAdmin) if err != nil { return reconcile.Result{}, errors.Wrapf(err, "unexpected error while synchronizing customize framework for %s", platformAdmin.Namespace+"/"+platformAdmin.Name) } @@ -374,38 +375,48 @@ func (r *ReconcilePlatformAdmin) reconcileConfigmap(ctx context.Context, platfor } func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminStatus *iotv1alpha2.PlatformAdminStatus, platformAdminFramework *PlatformAdminFramework) (bool, error) { - var desireComponents []*config.Component - needComponents := make(map[string]struct{}) - var readyComponent int32 = 0 - - desireComponents = platformAdminFramework.Components + var ( + readyComponent int32 = 0 + needComponents = make(map[string]struct{}) + ) + // TODO: The additional deployment and service of component is no longer supported in v1beta1. additionalComponents, err := annotationToComponent(platformAdmin.Annotations) if err != nil { return false, err } - desireComponents = append(desireComponents, additionalComponents...) - //TODO: handle PlatformAdmin.Spec.Components + // Users can configure components in the framework, + // or they can choose to configure optional components directly in spec, + // which combines the two approaches and tells the controller if the framework needs to be updated. + needWriteFramework := r.calculateDesiredComponents(platformAdmin, platformAdminFramework, additionalComponents) defer func() { platformAdminStatus.ReadyComponentNum = readyComponent - platformAdminStatus.UnreadyComponentNum = int32(len(desireComponents)) - readyComponent + platformAdminStatus.UnreadyComponentNum = int32(len(platformAdminFramework.Components)) - readyComponent }() - for _, desireComponent := range desireComponents { + // The component in spec that does not exist in the framework, so the framework needs to be updated. + if needWriteFramework { + if err := r.writeFramework(ctx, platformAdmin, platformAdminFramework); err != nil { + return false, err + } + } + + // Update the yurtappsets based on the desired components + for _, desiredComponent := range platformAdminFramework.Components { readyService := false readyDeployment := false - needComponents[desireComponent.Name] = struct{}{} + needComponents[desiredComponent.Name] = struct{}{} - if _, err := r.handleService(ctx, platformAdmin, desireComponent); err != nil { + if _, err := r.handleService(ctx, platformAdmin, desiredComponent); err != nil { return false, err } readyService = true yas := &appsv1alpha1.YurtAppSet{ ObjectMeta: metav1.ObjectMeta{ - Name: desireComponent.Name, + Name: desiredComponent.Name, Namespace: platformAdmin.Namespace, }, } @@ -414,13 +425,13 @@ func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platfor ctx, types.NamespacedName{ Namespace: platformAdmin.Namespace, - Name: desireComponent.Name}, + Name: desiredComponent.Name}, yas) if err != nil { if !apierrors.IsNotFound(err) { return false, err } - _, err = r.handleYurtAppSet(ctx, platformAdmin, desireComponent) + _, err = r.handleYurtAppSet(ctx, platformAdmin, desiredComponent) if err != nil { return false, err } @@ -428,7 +439,7 @@ func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platfor oldYas := yas.DeepCopy() // Refresh the YurtAppSet according to the user-defined configuration - yas.Spec.WorkloadTemplate.DeploymentTemplate.Spec = *desireComponent.Deployment + yas.Spec.WorkloadTemplate.DeploymentTemplate.Spec = *desiredComponent.Deployment if _, ok := yas.Status.PoolReplicas[platformAdmin.Spec.PoolName]; ok { if yas.Status.ReadyReplicas == yas.Status.Replicas { @@ -488,7 +499,7 @@ func (r *ReconcilePlatformAdmin) reconcileComponent(ctx context.Context, platfor } } - return readyComponent == int32(len(desireComponents)), nil + return readyComponent == int32(len(platformAdminFramework.Components)), nil } func (r *ReconcilePlatformAdmin) handleService(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, component *config.Component) (*corev1.Service, error) { @@ -648,7 +659,7 @@ func annotationToComponent(annotation map[string]string) ([]*config.Component, e return components, nil } -func (r *ReconcilePlatformAdmin) syncFramework(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin) (*PlatformAdminFramework, error) { +func (r *ReconcilePlatformAdmin) readFramework(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin) (*PlatformAdminFramework, error) { klog.V(6).Infof(Format("Synchronize the customize framework information for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) // Try to get the configmap that represents the framework @@ -715,6 +726,43 @@ func (r *ReconcilePlatformAdmin) syncFramework(ctx context.Context, platformAdmi return platformAdminFramework, nil } +func (r *ReconcilePlatformAdmin) writeFramework(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminFramework *PlatformAdminFramework) error { + // For better serialization, the serialization method of the Kubernetes runtime library is used + data, err := runtime.Encode(r.yamlSerializer, platformAdminFramework) + if err != nil { + klog.Errorf(Format("Failed to marshal framework for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + return err + } + + // Check if the configmap that represents framework is found + cm := &corev1.ConfigMap{} + if err := r.Get(ctx, types.NamespacedName{Namespace: platformAdmin.Namespace, Name: platformAdminFramework.name}, cm); err != nil { + if apierrors.IsNotFound(err) { + // If the configmap that represents framework is not found, + // need to create it by standard configuration + err = r.initFramework(ctx, platformAdmin, platformAdminFramework) + if err != nil { + klog.Errorf(Format("Init framework for PlatformAdmin %s/%s error %v", platformAdmin.Namespace, platformAdmin.Name, err)) + return err + } + return nil + } + klog.Errorf(Format("Get framework for PlatformAdmin %s/%s error %v", platformAdmin.Namespace, platformAdmin.Name, err)) + return err + } + + // Creates configmap on behalf of the framework, which is called only once upon creation + _, err = controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error { + cm.Data["framework"] = string(data) + return controllerutil.SetOwnerReference(platformAdmin, cm, r.Scheme()) + }) + if err != nil { + klog.Errorf(Format("Failed to write framework configmap for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + return err + } + return nil +} + // initFramework initializes the framework information for PlatformAdmin func (r *ReconcilePlatformAdmin) initFramework(ctx context.Context, platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminFramework *PlatformAdminFramework) error { klog.V(6).Infof(Format("Initializes the standard framework information for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) @@ -723,13 +771,13 @@ func (r *ReconcilePlatformAdmin) initFramework(ctx context.Context, platformAdmi platformAdminFramework.security = platformAdmin.Spec.Security if platformAdminFramework.security { platformAdminFramework.ConfigMaps = r.Configration.SecurityConfigMaps[platformAdmin.Spec.Version] - platformAdminFramework.Components = r.Configration.SecurityComponents[platformAdmin.Spec.Version] + r.calculateDesiredComponents(platformAdmin, platformAdminFramework, nil) } else { platformAdminFramework.ConfigMaps = r.Configration.NoSectyConfigMaps[platformAdmin.Spec.Version] - platformAdminFramework.Components = r.Configration.NoSectyComponents[platformAdmin.Spec.Version] + r.calculateDesiredComponents(platformAdmin, platformAdminFramework, nil) } - yurtIotDock, err := NewYurtIoTDockComponent(platformAdmin, platformAdminFramework) + yurtIotDock, err := newYurtIoTDockComponent(platformAdmin, platformAdminFramework) if err != nil { return err } @@ -761,8 +809,65 @@ func (r *ReconcilePlatformAdmin) initFramework(ctx context.Context, platformAdmi return controllerutil.SetOwnerReference(platformAdmin, cm, r.Scheme()) }) if err != nil { - klog.Errorf(Format("Failed to create or update framework configmap for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) + klog.Errorf(Format("Failed to init framework configmap for PlatformAdmin %s/%s", platformAdmin.Namespace, platformAdmin.Name)) return err } return nil } + +// calculateDesiredComponents calculates the components that need to be added and determines whether the framework needs to be rewritten +func (r *ReconcilePlatformAdmin) calculateDesiredComponents(platformAdmin *iotv1alpha2.PlatformAdmin, platformAdminFramework *PlatformAdminFramework, additionalComponents []*config.Component) bool { + needWriteFramework := false + desiredComponents := []*config.Component{} + + // Find all the required components from spec and manifest + requiredComponentSet := config.ExtractRequiredComponentsName(&r.Configration.Manifest, platformAdmin.Spec.Version) + for _, component := range platformAdmin.Spec.Components { + requiredComponentSet.Insert(component.Name) + } + + // Find all existing components and filter removed components + frameworkComponentSet := sets.NewString() + for _, component := range platformAdminFramework.Components { + if requiredComponentSet.Has(component.Name) { + frameworkComponentSet.Insert(component.Name) + desiredComponents = append(desiredComponents, component) + } else { + needWriteFramework = true + } + } + + // Calculate all the components that need to be added or removed and determine whether need to rewrite the framework + addedComponentSet := sets.NewString() + for _, componentName := range requiredComponentSet.List() { + if !frameworkComponentSet.Has(componentName) { + addedComponentSet.Insert(componentName) + needWriteFramework = true + } + } + + // If a component needs to be added, + // check whether the corresponding template exists in the standard configuration library + if platformAdmin.Spec.Security { + for _, component := range r.Configration.SecurityComponents[platformAdmin.Spec.Version] { + if addedComponentSet.Has(component.Name) { + desiredComponents = append(desiredComponents, component) + } + } + } else { + for _, component := range r.Configration.NoSectyComponents[platformAdmin.Spec.Version] { + if addedComponentSet.Has(component.Name) { + desiredComponents = append(desiredComponents, component) + } + } + } + + // TODO: In order to be compatible with v1alpha1, we need to add the component from annotation translation here + if additionalComponents != nil { + desiredComponents = append(desiredComponents, additionalComponents...) + } + + platformAdminFramework.Components = desiredComponents + + return needWriteFramework +} diff --git a/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_handler.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_handler.go index 7dc5b2e6170..670e9234f4d 100644 --- a/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_handler.go +++ b/pkg/yurtmanager/webhook/platformadmin/v1alpha1/platformadmin_handler.go @@ -26,16 +26,9 @@ import ( "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" - "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" + webhookutil "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) -type Manifest struct { - Updated string `yaml:"updated"` - Count int `yaml:"count"` - LatestVersion string `yaml:"latestVersion"` - Versions []string `yaml:"versions"` -} - // SetupWebhookWithManager sets up Cluster webhooks. func (webhook *PlatformAdminHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { // init @@ -50,8 +43,8 @@ func (webhook *PlatformAdminHandler) SetupWebhookWithManager(mgr ctrl.Manager) ( return "", "", err } - return util.GenerateMutatePath(gvk), - util.GenerateValidatePath(gvk), + return webhookutil.GenerateMutatePath(gvk), + webhookutil.GenerateValidatePath(gvk), ctrl.NewWebhookManagedBy(mgr). For(&v1alpha1.PlatformAdmin{}). WithDefaulter(webhook). @@ -60,7 +53,7 @@ func (webhook *PlatformAdminHandler) SetupWebhookWithManager(mgr ctrl.Manager) ( } func (webhook *PlatformAdminHandler) initManifest() error { - webhook.Manifests = &Manifest{} + webhook.Manifests = &config.Manifest{} manifestContent, err := config.EdgeXFS.ReadFile(config.ManifestPath) if err != nil { @@ -79,7 +72,7 @@ func (webhook *PlatformAdminHandler) initManifest() error { // Cluster implements a validating and defaulting webhook for Cluster. type PlatformAdminHandler struct { Client client.Client - Manifests *Manifest + Manifests *config.Manifest } var _ webhook.CustomDefaulter = &PlatformAdminHandler{} diff --git a/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_handler.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_handler.go index aa707e3e879..547e9149385 100644 --- a/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_handler.go +++ b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_handler.go @@ -26,16 +26,9 @@ import ( "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" - "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" + webhookutil "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) -type Manifest struct { - Updated string `yaml:"updated"` - Count int `yaml:"count"` - LatestVersion string `yaml:"latestVersion"` - Versions []string `yaml:"versions"` -} - // SetupWebhookWithManager sets up Cluster webhooks. func (webhook *PlatformAdminHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { // init @@ -50,8 +43,8 @@ func (webhook *PlatformAdminHandler) SetupWebhookWithManager(mgr ctrl.Manager) ( return "", "", err } - return util.GenerateMutatePath(gvk), - util.GenerateValidatePath(gvk), + return webhookutil.GenerateMutatePath(gvk), + webhookutil.GenerateValidatePath(gvk), ctrl.NewWebhookManagedBy(mgr). For(&v1alpha2.PlatformAdmin{}). WithDefaulter(webhook). @@ -60,7 +53,7 @@ func (webhook *PlatformAdminHandler) SetupWebhookWithManager(mgr ctrl.Manager) ( } func (webhook *PlatformAdminHandler) initManifest() error { - webhook.Manifests = &Manifest{} + webhook.Manifests = &config.Manifest{} manifestContent, err := config.EdgeXFS.ReadFile(config.ManifestPath) if err != nil { @@ -82,7 +75,7 @@ func (webhook *PlatformAdminHandler) initManifest() error { // Cluster implements a validating and defaulting webhook for Cluster. type PlatformAdminHandler struct { Client client.Client - Manifests *Manifest + Manifests *config.Manifest } var _ webhook.CustomDefaulter = &PlatformAdminHandler{} diff --git a/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_validation.go b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_validation.go index 8525c13db6d..3f5d422cd46 100644 --- a/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_validation.go +++ b/pkg/yurtmanager/webhook/platformadmin/v1alpha2/platformadmin_validation.go @@ -28,6 +28,7 @@ import ( unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha2" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/config" util "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin/utils" ) @@ -94,13 +95,13 @@ func (webhook *PlatformAdminHandler) validatePlatformAdminSpec(platformAdmin *v1 // Verify that it is a supported platformadmin version for _, version := range webhook.Manifests.Versions { - if platformAdmin.Spec.Version == version { + if platformAdmin.Spec.Version == version.Name { return nil } } return field.ErrorList{ - field.Invalid(field.NewPath("spec", "version"), platformAdmin.Spec.Version, "must be one of"+strings.Join(webhook.Manifests.Versions, ",")), + field.Invalid(field.NewPath("spec", "version"), platformAdmin.Spec.Version, "must be one of"+strings.Join(config.ExtractVersionsName(webhook.Manifests).List(), ",")), } } From bc354eaeeb7767c909dd29f5bd243e78335c3f40 Mon Sep 17 00:00:00 2001 From: Abyss <45425302+wangxye@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:05:24 +0800 Subject: [PATCH 82/93] feat: upgrade YurtIoTDock to support edgex v3 api (#1666) * update the client of iotdock to deploy edgex according to different versions Signed-off-by: wangxye <1031989637@qq.com> * add v3 client to support edgex deployment of v3 version Signed-off-by: wangxye <1031989637@qq.com> --------- Signed-off-by: wangxye <1031989637@qq.com> --- cmd/yurt-iot-dock/app/core.go | 15 +- cmd/yurt-iot-dock/app/options/options.go | 3 + go.mod | 9 +- go.sum | 32 +- .../clients/edgex-foundry/edgexobject.go | 55 ++ .../edgex-foundry/{ => v2}/device_client.go | 2 +- .../{ => v2}/device_client_test.go | 2 +- .../{ => v2}/deviceprofile_client.go | 2 +- .../{ => v2}/deviceprofile_client_test.go | 2 +- .../{ => v2}/deviceservice_client.go | 2 +- .../{ => v2}/deviceservice_client_test.go | 2 +- .../clients/edgex-foundry/{ => v2}/util.go | 2 +- .../clients/edgex-foundry/v3/device_client.go | 371 ++++++++++++++ .../edgex-foundry/v3/device_client_test.go | 202 ++++++++ .../edgex-foundry/v3/deviceprofile_client.go | 141 ++++++ .../v3/deviceprofile_client_test.go | 107 ++++ .../edgex-foundry/v3/deviceservice_client.go | 166 ++++++ .../v3/deviceservice_client_test.go | 126 +++++ .../clients/edgex-foundry/v3/util.go | 472 ++++++++++++++++++ pkg/yurtiotdock/clients/interface.go | 7 + .../controllers/device_controller.go | 10 +- pkg/yurtiotdock/controllers/device_syncer.go | 10 +- .../controllers/deviceprofile_controller.go | 10 +- .../controllers/deviceprofile_syncer.go | 10 +- .../controllers/deviceservice_controller.go | 10 +- .../controllers/deviceservice_syncer.go | 10 +- pkg/yurtiotdock/controllers/util/string.go | 56 +++ .../controller/platformadmin/iotdock.go | 1 + 28 files changed, 1782 insertions(+), 55 deletions(-) rename pkg/yurtiotdock/clients/edgex-foundry/{ => v2}/device_client.go (99%) rename pkg/yurtiotdock/clients/edgex-foundry/{ => v2}/device_client_test.go (99%) rename pkg/yurtiotdock/clients/edgex-foundry/{ => v2}/deviceprofile_client.go (99%) rename pkg/yurtiotdock/clients/edgex-foundry/{ => v2}/deviceprofile_client_test.go (99%) rename pkg/yurtiotdock/clients/edgex-foundry/{ => v2}/deviceservice_client.go (99%) rename pkg/yurtiotdock/clients/edgex-foundry/{ => v2}/deviceservice_client_test.go (99%) rename pkg/yurtiotdock/clients/edgex-foundry/{ => v2}/util.go (99%) create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/v3/device_client.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/v3/device_client_test.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/v3/deviceprofile_client.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/v3/deviceprofile_client_test.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/v3/deviceservice_client.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/v3/deviceservice_client_test.go create mode 100644 pkg/yurtiotdock/clients/edgex-foundry/v3/util.go diff --git a/cmd/yurt-iot-dock/app/core.go b/cmd/yurt-iot-dock/app/core.go index 3d4aae34219..061dedbfe9e 100644 --- a/cmd/yurt-iot-dock/app/core.go +++ b/cmd/yurt-iot-dock/app/core.go @@ -37,6 +37,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" "github.com/openyurtio/openyurt/pkg/apis" + edgexclients "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers" "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" ) @@ -116,15 +117,17 @@ func Run(opts *options.YurtIoTDockOptions, stopCh <-chan struct{}) { } } + edgexdock := edgexclients.NewEdgexDock(opts.Version, opts.CoreMetadataAddr, opts.CoreCommandAddr) + // setup the DeviceProfile Reconciler and Syncer if err = (&controllers.DeviceProfileReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr, opts); err != nil { + }).SetupWithManager(mgr, opts, edgexdock); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DeviceProfile") os.Exit(1) } - dfs, err := controllers.NewDeviceProfileSyncer(mgr.GetClient(), opts) + dfs, err := controllers.NewDeviceProfileSyncer(mgr.GetClient(), opts, edgexdock) if err != nil { setupLog.Error(err, "unable to create syncer", "syncer", "DeviceProfile") os.Exit(1) @@ -139,11 +142,11 @@ func Run(opts *options.YurtIoTDockOptions, stopCh <-chan struct{}) { if err = (&controllers.DeviceReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr, opts); err != nil { + }).SetupWithManager(mgr, opts, edgexdock); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Device") os.Exit(1) } - ds, err := controllers.NewDeviceSyncer(mgr.GetClient(), opts) + ds, err := controllers.NewDeviceSyncer(mgr.GetClient(), opts, edgexdock) if err != nil { setupLog.Error(err, "unable to create syncer", "controller", "Device") os.Exit(1) @@ -158,11 +161,11 @@ func Run(opts *options.YurtIoTDockOptions, stopCh <-chan struct{}) { if err = (&controllers.DeviceServiceReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr, opts); err != nil { + }).SetupWithManager(mgr, opts, edgexdock); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DeviceService") os.Exit(1) } - dss, err := controllers.NewDeviceServiceSyncer(mgr.GetClient(), opts) + dss, err := controllers.NewDeviceServiceSyncer(mgr.GetClient(), opts, edgexdock) if err != nil { setupLog.Error(err, "unable to create syncer", "syncer", "DeviceService") os.Exit(1) diff --git a/cmd/yurt-iot-dock/app/options/options.go b/cmd/yurt-iot-dock/app/options/options.go index 08d9e37a920..747cbf389ca 100644 --- a/cmd/yurt-iot-dock/app/options/options.go +++ b/cmd/yurt-iot-dock/app/options/options.go @@ -30,6 +30,7 @@ type YurtIoTDockOptions struct { EnableLeaderElection bool Nodepool string Namespace string + Version string CoreDataAddr string CoreMetadataAddr string CoreCommandAddr string @@ -43,6 +44,7 @@ func NewYurtIoTDockOptions() *YurtIoTDockOptions { EnableLeaderElection: false, Nodepool: "", Namespace: "default", + Version: "", CoreDataAddr: "edgex-core-data:59880", CoreMetadataAddr: "edgex-core-metadata:59881", CoreCommandAddr: "edgex-core-command:59882", @@ -63,6 +65,7 @@ func (o *YurtIoTDockOptions) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&o.EnableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+"Enabling this will ensure there is only one active controller manager.") fs.StringVar(&o.Nodepool, "nodepool", "", "The nodePool deviceController is deployed in.(just for debugging)") fs.StringVar(&o.Namespace, "namespace", "default", "The cluster namespace for edge resources synchronization.") + fs.StringVar(&o.Version, "version", "", "The version of edge resources deploymenet.") fs.StringVar(&o.CoreDataAddr, "core-data-address", "edgex-core-data:59880", "The address of edge core-data service.") fs.StringVar(&o.CoreMetadataAddr, "core-metadata-address", "edgex-core-metadata:59881", "The address of edge core-metadata service.") fs.StringVar(&o.CoreCommandAddr, "core-command-address", "edgex-core-command:59882", "The address of edge core-command service.") diff --git a/go.mod b/go.mod index 4e81d05d27c..6bdad04c789 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/aliyun/alibaba-cloud-sdk-go v1.62.156 github.com/davecgh/go-spew v1.1.1 github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0 + github.com/edgexfoundry/go-mod-core-contracts/v3 v3.0.0 github.com/go-resty/resty/v2 v2.7.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.0 @@ -95,9 +96,9 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/go-playground/validator/v10 v10.11.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.13.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -111,7 +112,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.2.1 // indirect + github.com/leodido/go-urn v1.2.3 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect diff --git a/go.sum b/go.sum index 91b67aef842..e3fa03ef033 100644 --- a/go.sum +++ b/go.sum @@ -177,6 +177,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0 h1:8Svk1HTehXEgwxgyA4muVhSkP3D9n1q+oSHI3B1Ac90= github.com/edgexfoundry/go-mod-core-contracts/v2 v2.3.0/go.mod h1:4/e61acxVkhQWCTjQ4XcHVJDnrMDloFsZZB1B6STCRw= +github.com/edgexfoundry/go-mod-core-contracts/v3 v3.0.0 h1:xjwCI34DLM31cSl1q9XmYgXS3JqXufQJMgohnLLLDx0= +github.com/edgexfoundry/go-mod-core-contracts/v3 v3.0.0/go.mod h1:zzzWGWij6wAqm1go9TLs++TFMIsBqBb1eRnIj4mRxGw= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -248,14 +250,13 @@ github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfT github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= -github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ= +github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -436,7 +437,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -444,8 +444,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kubernetes/kubernetes v1.22.3 h1:0gYnqsr5nZiAO+iDkEU7RJ6Ne2CMyoinJXVm5qVSTiE= github.com/kubernetes/kubernetes v1.22.3/go.mod h1:Snea7fgIObGgHmLbUJ3OgjGEr5bjj16iEdp5oHS6eS8= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= +github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= @@ -557,7 +557,6 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -606,8 +605,6 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= @@ -670,6 +667,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -775,7 +773,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -858,7 +855,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -936,10 +932,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -960,7 +954,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1115,7 +1108,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go b/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go index 4a093bf9834..93159fb31aa 100644 --- a/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go +++ b/pkg/yurtiotdock/clients/edgex-foundry/edgexobject.go @@ -16,6 +16,61 @@ limitations under the License. package edgex_foundry +import ( + "fmt" + + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" + edgexcliv2 "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry/v2" + edgexcliv3 "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry/v3" +) + type EdgeXObject interface { IsAddedToEdgeX() bool } + +type EdgexDock struct { + Version string + CoreMetadataAddr string + CoreCommandAddr string +} + +func NewEdgexDock(version string, coreMetadataAddr string, coreCommandAddr string) *EdgexDock { + return &EdgexDock{ + Version: version, + CoreMetadataAddr: coreMetadataAddr, + CoreCommandAddr: coreCommandAddr, + } +} + +func (ep *EdgexDock) CreateDeviceClient() (clients.DeviceInterface, error) { + switch ep.Version { + case "minnesota": + return edgexcliv3.NewEdgexDeviceClient(ep.CoreMetadataAddr, ep.CoreCommandAddr), nil + case "levski", "kamakura", "jakarta": + return edgexcliv2.NewEdgexDeviceClient(ep.CoreMetadataAddr, ep.CoreCommandAddr), nil + default: + return nil, fmt.Errorf("unsupported Edgex version: %v", ep.Version) + } +} + +func (ep *EdgexDock) CreateDeviceProfileClient() (clients.DeviceProfileInterface, error) { + switch ep.Version { + case "minnesota": + return edgexcliv3.NewEdgexDeviceProfile(ep.CoreMetadataAddr), nil + case "levski", "kamakura", "jakarta": + return edgexcliv2.NewEdgexDeviceProfile(ep.CoreMetadataAddr), nil + default: + return nil, fmt.Errorf("unsupported Edgex version: %v", ep.Version) + } +} + +func (ep *EdgexDock) CreateDeviceServiceClient() (clients.DeviceServiceInterface, error) { + switch ep.Version { + case "minnesota": + return edgexcliv3.NewEdgexDeviceServiceClient(ep.CoreMetadataAddr), nil + case "levski", "kamakura", "jakarta": + return edgexcliv2.NewEdgexDeviceServiceClient(ep.CoreMetadataAddr), nil + default: + return nil, fmt.Errorf("unsupported Edgex version: %v", ep.Version) + } +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/device_client.go b/pkg/yurtiotdock/clients/edgex-foundry/v2/device_client.go similarity index 99% rename from pkg/yurtiotdock/clients/edgex-foundry/device_client.go rename to pkg/yurtiotdock/clients/edgex-foundry/v2/device_client.go index 72ef381d8fa..771d1659f43 100644 --- a/pkg/yurtiotdock/clients/edgex-foundry/device_client.go +++ b/pkg/yurtiotdock/clients/edgex-foundry/v2/device_client.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package edgex_foundry +package v2 import ( "context" diff --git a/pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/v2/device_client_test.go similarity index 99% rename from pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go rename to pkg/yurtiotdock/clients/edgex-foundry/v2/device_client_test.go index 5873a540d74..f34d5b76c6a 100644 --- a/pkg/yurtiotdock/clients/edgex-foundry/device_client_test.go +++ b/pkg/yurtiotdock/clients/edgex-foundry/v2/device_client_test.go @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package edgex_foundry +package v2 import ( "context" diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go b/pkg/yurtiotdock/clients/edgex-foundry/v2/deviceprofile_client.go similarity index 99% rename from pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go rename to pkg/yurtiotdock/clients/edgex-foundry/v2/deviceprofile_client.go index 2cd9c9d40c6..8f300278cf5 100644 --- a/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client.go +++ b/pkg/yurtiotdock/clients/edgex-foundry/v2/deviceprofile_client.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package edgex_foundry +package v2 import ( "context" diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/v2/deviceprofile_client_test.go similarity index 99% rename from pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go rename to pkg/yurtiotdock/clients/edgex-foundry/v2/deviceprofile_client_test.go index 0f32eecf687..ba6e2599021 100644 --- a/pkg/yurtiotdock/clients/edgex-foundry/deviceprofile_client_test.go +++ b/pkg/yurtiotdock/clients/edgex-foundry/v2/deviceprofile_client_test.go @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package edgex_foundry +package v2 import ( "context" diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go b/pkg/yurtiotdock/clients/edgex-foundry/v2/deviceservice_client.go similarity index 99% rename from pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go rename to pkg/yurtiotdock/clients/edgex-foundry/v2/deviceservice_client.go index c7cd9f70f24..48d74e223ce 100644 --- a/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client.go +++ b/pkg/yurtiotdock/clients/edgex-foundry/v2/deviceservice_client.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package edgex_foundry +package v2 import ( "context" diff --git a/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/v2/deviceservice_client_test.go similarity index 99% rename from pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go rename to pkg/yurtiotdock/clients/edgex-foundry/v2/deviceservice_client_test.go index c00d7b9118e..def719cf836 100644 --- a/pkg/yurtiotdock/clients/edgex-foundry/deviceservice_client_test.go +++ b/pkg/yurtiotdock/clients/edgex-foundry/v2/deviceservice_client_test.go @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -package edgex_foundry +package v2 import ( "context" diff --git a/pkg/yurtiotdock/clients/edgex-foundry/util.go b/pkg/yurtiotdock/clients/edgex-foundry/v2/util.go similarity index 99% rename from pkg/yurtiotdock/clients/edgex-foundry/util.go rename to pkg/yurtiotdock/clients/edgex-foundry/v2/util.go index 8fbf276ab30..40753cda5bd 100644 --- a/pkg/yurtiotdock/clients/edgex-foundry/util.go +++ b/pkg/yurtiotdock/clients/edgex-foundry/v2/util.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package edgex_foundry +package v2 import ( "fmt" diff --git a/pkg/yurtiotdock/clients/edgex-foundry/v3/device_client.go b/pkg/yurtiotdock/clients/edgex-foundry/v3/device_client.go new file mode 100644 index 00000000000..609c99fe8bf --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/v3/device_client.go @@ -0,0 +1,371 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/cookiejar" + "strings" + "time" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/go-resty/resty/v2" + "golang.org/x/net/publicsuffix" + "k8s.io/klog/v2" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceClient struct { + *resty.Client + CoreMetaAddr string + CoreCommandAddr string +} + +func NewEdgexDeviceClient(coreMetaAddr, coreCommandAddr string) *EdgexDeviceClient { + cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + instance := resty.NewWithClient(&http.Client{ + Jar: cookieJar, + Timeout: 10 * time.Second, + }) + return &EdgexDeviceClient{ + Client: instance, + CoreMetaAddr: coreMetaAddr, + CoreCommandAddr: coreCommandAddr, + } +} + +// Create function sends a POST request to EdgeX to add a new device +func (efc *EdgexDeviceClient) Create(ctx context.Context, device *iotv1alpha1.Device, options clients.CreateOptions) (*iotv1alpha1.Device, error) { + devs := []*iotv1alpha1.Device{device} + req := makeEdgeXDeviceRequest(devs) + klog.V(5).Infof("will add the Device: %s", device.Name) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postPath := fmt.Sprintf("http://%s%s", efc.CoreMetaAddr, DevicePath) + resp, err := efc.R(). + SetBody(reqBody).Post(postPath) + if err != nil { + return nil, err + } else if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create device on edgex foundry failed, the response is : %s", resp.Body()) + } + + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDevice := device.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDevice.Status.EdgeId = edgexResps[0].Id + createdDevice.Status.Synced = true + } else { + return nil, fmt.Errorf("create device on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch device cound, the response is : %s", resp.Body()) + } + return createdDevice, err +} + +// Delete function sends a request to EdgeX to delete a device +func (efc *EdgexDeviceClient) Delete(ctx context.Context, name string, options clients.DeleteOptions) error { + klog.V(5).Infof("will delete the Device: %s", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreMetaAddr, DevicePath, name) + resp, err := efc.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return errors.New(string(resp.Body())) + } + return nil +} + +// Update is used to set the admin or operating state of the device by unique name of the device. +// TODO support to update other fields +func (efc *EdgexDeviceClient) Update(ctx context.Context, device *iotv1alpha1.Device, options clients.UpdateOptions) (*iotv1alpha1.Device, error) { + actualDeviceName := getEdgeXName(device) + patchURL := fmt.Sprintf("http://%s%s", efc.CoreMetaAddr, DevicePath) + if device == nil { + return nil, nil + } + devs := []*iotv1alpha1.Device{device} + req := makeEdgeXDeviceUpdateRequest(devs) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + rep, err := efc.R(). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + Patch(patchURL) + if err != nil { + return nil, err + } else if rep.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("failed to update device: %s, get response: %s", actualDeviceName, string(rep.Body())) + } + return device, nil +} + +// Get is used to query the device information corresponding to the device name +func (efc *EdgexDeviceClient) Get(ctx context.Context, deviceName string, options clients.GetOptions) (*iotv1alpha1.Device, error) { + klog.V(5).Infof("will get Devices: %s", deviceName) + var dResp edgex_resp.DeviceResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreMetaAddr, DevicePath, deviceName) + resp, err := efc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("Device %s not found", deviceName) + } + err = json.Unmarshal(resp.Body(), &dResp) + if err != nil { + return nil, err + } + device := toKubeDevice(dResp.Device, options.Namespace) + return &device, err +} + +// List is used to get all device objects on edge platform +// TODO:support label filtering according to options +func (efc *EdgexDeviceClient) List(ctx context.Context, options clients.ListOptions) ([]iotv1alpha1.Device, error) { + lp := fmt.Sprintf("http://%s%s/all?limit=-1", efc.CoreMetaAddr, DevicePath) + resp, err := efc.R().EnableTrace().Get(lp) + if err != nil { + return nil, err + } + var mdResp edgex_resp.MultiDevicesResponse + if err := json.Unmarshal(resp.Body(), &mdResp); err != nil { + return nil, err + } + var res []iotv1alpha1.Device + for _, dp := range mdResp.Devices { + res = append(res, toKubeDevice(dp, options.Namespace)) + } + return res, nil +} + +func (efc *EdgexDeviceClient) GetPropertyState(ctx context.Context, propertyName string, d *iotv1alpha1.Device, options clients.GetOptions) (*iotv1alpha1.ActualPropertyState, error) { + actualDeviceName := getEdgeXName(d) + // get the old property from status + oldAps, exist := d.Status.DeviceProperties[propertyName] + propertyGetURL := "" + // 1. query the Get URL of a property + if !exist || (exist && oldAps.GetURL == "") { + coreCommands, err := efc.GetCommandResponseByName(actualDeviceName) + if err != nil { + return &iotv1alpha1.ActualPropertyState{}, err + } + for _, c := range coreCommands { + if c.Name == propertyName && c.Get { + propertyGetURL = fmt.Sprintf("%s%s", c.Url, c.Path) + break + } + } + if propertyGetURL == "" { + return nil, &clients.NotFoundError{} + } + } else { + propertyGetURL = oldAps.GetURL + } + // 2. get the actual property value by the getURL + actualPropertyState := iotv1alpha1.ActualPropertyState{ + Name: propertyName, + GetURL: propertyGetURL, + } + if resp, err := efc.getPropertyState(propertyGetURL); err != nil { + return nil, err + } else { + var eResp edgex_resp.EventResponse + if err := json.Unmarshal(resp.Body(), &eResp); err != nil { + return nil, err + } + actualPropertyState.ActualValue = getPropertyValueFromEvent(propertyName, eResp.Event) + } + return &actualPropertyState, nil +} + +// getPropertyState returns different error messages according to the status code +func (efc *EdgexDeviceClient) getPropertyState(getURL string) (*resty.Response, error) { + resp, err := efc.R().Get(getURL) + if err != nil { + return resp, err + } + if resp.StatusCode() == 400 { + err = errors.New("request is in an invalid state") + } else if resp.StatusCode() == 404 { + err = errors.New("the requested resource does not exist") + } else if resp.StatusCode() == 423 { + err = errors.New("the device is locked (AdminState) or down (OperatingState)") + } else if resp.StatusCode() == 500 { + err = errors.New("an unexpected error occurred on the server") + } + return resp, err +} + +func (efc *EdgexDeviceClient) UpdatePropertyState(ctx context.Context, propertyName string, d *iotv1alpha1.Device, options clients.UpdateOptions) error { + // Get the actual device name + acturalDeviceName := getEdgeXName(d) + + dps := d.Spec.DeviceProperties[propertyName] + parameterName := dps.Name + if dps.PutURL == "" { + putCmd, err := efc.getPropertyPut(acturalDeviceName, dps.Name) + if err != nil { + return err + } + dps.PutURL = fmt.Sprintf("%s%s", putCmd.Url, putCmd.Path) + if len(putCmd.Parameters) == 1 { + parameterName = putCmd.Parameters[0].ResourceName + } + } + // set the device property to desired state + bodyMap := make(map[string]string) + bodyMap[parameterName] = dps.DesiredValue + body, _ := json.Marshal(bodyMap) + klog.V(5).Infof("setting the property to desired value", "propertyName", parameterName, "desiredValue", string(body)) + rep, err := efc.R(). + SetHeader("Content-Type", "application/json"). + SetBody(body). + Put(dps.PutURL) + if err != nil { + return err + } else if rep.StatusCode() != http.StatusOK { + return fmt.Errorf("failed to set property: %s, get response: %s", dps.Name, string(rep.Body())) + } else if rep.Body() != nil { + // If the parameters are illegal, such as out of range, the 200 status code is also returned, but the description appears in the body + a := string(rep.Body()) + if strings.Contains(a, "execWriteCmd") { + return fmt.Errorf("failed to set property: %s, get response: %s", dps.Name, string(rep.Body())) + } + } + return nil +} + +// Gets the models.Put from edgex foundry which is used to set the device property's value +func (efc *EdgexDeviceClient) getPropertyPut(deviceName, cmdName string) (dtos.CoreCommand, error) { + coreCommands, err := efc.GetCommandResponseByName(deviceName) + if err != nil { + return dtos.CoreCommand{}, err + } + for _, c := range coreCommands { + if cmdName == c.Name && c.Set { + return c, nil + } + } + return dtos.CoreCommand{}, errors.New("corresponding command is not found") +} + +// ListPropertiesState gets all the actual property information about a device +func (efc *EdgexDeviceClient) ListPropertiesState(ctx context.Context, device *iotv1alpha1.Device, options clients.ListOptions) (map[string]iotv1alpha1.DesiredPropertyState, map[string]iotv1alpha1.ActualPropertyState, error) { + actualDeviceName := getEdgeXName(device) + + dpsm := map[string]iotv1alpha1.DesiredPropertyState{} + apsm := map[string]iotv1alpha1.ActualPropertyState{} + coreCommands, err := efc.GetCommandResponseByName(actualDeviceName) + if err != nil { + return dpsm, apsm, err + } + + for _, c := range coreCommands { + // DesiredPropertyState only store the basic information and does not set DesiredValue + if c.Get { + getURL := fmt.Sprintf("%s%s", c.Url, c.Path) + aps, ok := apsm[c.Name] + if ok { + aps.GetURL = getURL + } else { + aps = iotv1alpha1.ActualPropertyState{Name: c.Name, GetURL: getURL} + } + apsm[c.Name] = aps + resp, err := efc.getPropertyState(getURL) + if err != nil { + klog.V(5).ErrorS(err, "getPropertyState failed", "propertyName", c.Name, "deviceName", actualDeviceName) + } else { + var eResp edgex_resp.EventResponse + if err := json.Unmarshal(resp.Body(), &eResp); err != nil { + klog.V(5).ErrorS(err, "failed to decode the response ", "response", resp) + continue + } + event := eResp.Event + readingName := c.Name + expectParams := c.Parameters + if len(expectParams) == 1 { + readingName = expectParams[0].ResourceName + } + klog.V(5).Infof("get reading name %s for command %s of device %s", readingName, c.Name, device.Name) + actualValue := getPropertyValueFromEvent(readingName, event) + aps.ActualValue = actualValue + apsm[c.Name] = aps + } + } + } + return dpsm, apsm, nil +} + +// The actual property value is resolved from the returned event +func getPropertyValueFromEvent(resName string, event dtos.Event) string { + actualValue := "" + for _, r := range event.Readings { + if resName == r.ResourceName { + if r.SimpleReading.Value != "" { + actualValue = r.SimpleReading.Value + } else if len(r.BinaryReading.BinaryValue) != 0 { + // TODO: how to demonstrate binary data + actualValue = fmt.Sprintf("%s:%s", r.BinaryReading.MediaType, "blob value") + } else if r.ObjectReading.ObjectValue != nil { + serializedBytes, _ := json.Marshal(r.ObjectReading.ObjectValue) + actualValue = string(serializedBytes) + } + break + } + } + return actualValue +} + +// GetCommandResponseByName gets all commands supported by the device +func (efc *EdgexDeviceClient) GetCommandResponseByName(deviceName string) ([]dtos.CoreCommand, error) { + klog.V(5).Infof("will get CommandResponses of device: %s", deviceName) + + var dcr edgex_resp.DeviceCoreCommandResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", efc.CoreCommandAddr, CommandResponsePath, deviceName) + + resp, err := efc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, errors.New("Item not found") + } + err = json.Unmarshal(resp.Body(), &dcr) + if err != nil { + return nil, err + } + return dcr.DeviceCoreCommand.CoreCommands, nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/v3/device_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/v3/device_client_test.go new file mode 100644 index 00000000000..4f6191e9c10 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/v3/device_client_test.go @@ -0,0 +1,202 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v3 + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceListMetadata = `{"apiVersion":"v3","statusCode":200,"totalCount":5,"devices":[{"created":1661829206505,"modified":1661829206505,"id":"f6255845-f4b2-4182-bd3c-abc9eac4a649","name":"Random-Float-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Float-Device","autoEvents":[{"interval":"30s","onChange":false,"sourceName":"Float32"},{"interval":"30s","onChange":false,"sourceName":"Float64"}],"protocols":{"other":{"Address":"device-virtual-float-01","Protocol":"300"}}},{"created":1661829206506,"modified":1661829206506,"id":"d29efe20-fdec-4aeb-90e5-99528cb6ca28","name":"Random-Binary-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Binary-Device","protocols":{"other":{"Address":"device-virtual-binary-01","Port":"300"}}},{"created":1661829206504,"modified":1661829206504,"id":"6a7f00a4-9536-48b2-9380-a9fc202ac517","name":"Random-Integer-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Integer-Device","autoEvents":[{"interval":"15s","onChange":false,"sourceName":"Int8"},{"interval":"15s","onChange":false,"sourceName":"Int16"},{"interval":"15s","onChange":false,"sourceName":"Int32"},{"interval":"15s","onChange":false,"sourceName":"Int64"}],"protocols":{"other":{"Address":"device-virtual-int-01","Protocol":"300"}}},{"created":1661829206503,"modified":1661829206503,"id":"439d47a2-fa72-4c27-9f47-c19356cc0c3b","name":"Random-Boolean-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Boolean-Device","autoEvents":[{"interval":"10s","onChange":false,"sourceName":"Bool"}],"protocols":{"other":{"Address":"device-virtual-bool-01","Port":"300"}}},{"created":1661829206505,"modified":1661829206505,"id":"2890ab86-3ae4-4b5e-98ab-aad85fc540e6","name":"Random-UnsignedInteger-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-UnsignedInteger-Device","autoEvents":[{"interval":"20s","onChange":false,"sourceName":"Uint8"},{"interval":"20s","onChange":false,"sourceName":"Uint16"},{"interval":"20s","onChange":false,"sourceName":"Uint32"},{"interval":"20s","onChange":false,"sourceName":"Uint64"}],"protocols":{"other":{"Address":"device-virtual-uint-01","Protocol":"300"}}}]}` + DeviceMetadata = `{"apiVersion":"v3","statusCode":200,"device":{"created":1661829206505,"modified":1661829206505,"id":"f6255845-f4b2-4182-bd3c-abc9eac4a649","name":"Random-Float-Device","description":"Example of Device Virtual","adminState":"UNLOCKED","operatingState":"UP","labels":["device-virtual-example"],"serviceName":"device-virtual","profileName":"Random-Float-Device","autoEvents":[{"interval":"30s","onChange":false,"sourceName":"Float32"},{"interval":"30s","onChange":false,"sourceName":"Float64"}],"protocols":{"other":{"Address":"device-virtual-float-01","Protocol":"300"}}}}` + + DeviceCreateSuccess = `[{"apiVersion":"v3","statusCode":201,"id":"2fff4f1a-7110-442f-b347-9f896338ba57"}]` + DeviceCreateFail = `[{"apiVersion":"v3","message":"device name test-Random-Float-Device already exists","statusCode":409}]` + + DeviceDeleteSuccess = `{"apiVersion":"v3","statusCode":200}` + DeviceDeleteFail = `{"apiVersion":"v3","message":"fail to query device by name test-Random-Float-Device","statusCode":404}` + + DeviceCoreCommands = `{"apiVersion":"v3","statusCode":200,"deviceCoreCommand":{"deviceName":"Random-Float-Device","profileName":"Random-Float-Device","coreCommands":[{"name":"WriteFloat32ArrayValue","set":true,"path":"/api/v3/device/name/Random-Float-Device/WriteFloat32ArrayValue","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32Array","valueType":"Float32Array"},{"resourceName":"EnableRandomization_Float32Array","valueType":"Bool"}]},{"name":"WriteFloat64ArrayValue","set":true,"path":"/api/v3/device/name/Random-Float-Device/WriteFloat64ArrayValue","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64Array","valueType":"Float64Array"},{"resourceName":"EnableRandomization_Float64Array","valueType":"Bool"}]},{"name":"Float32","get":true,"set":true,"path":"/api/v3/device/name/Random-Float-Device/Float32","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32","valueType":"Float32"}]},{"name":"Float64","get":true,"set":true,"path":"/api/v3/device/name/Random-Float-Device/Float64","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64","valueType":"Float64"}]},{"name":"Float32Array","get":true,"set":true,"path":"/api/v3/device/name/Random-Float-Device/Float32Array","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32Array","valueType":"Float32Array"}]},{"name":"Float64Array","get":true,"set":true,"path":"/api/v3/device/name/Random-Float-Device/Float64Array","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64Array","valueType":"Float64Array"}]},{"name":"WriteFloat32Value","set":true,"path":"/api/v3/device/name/Random-Float-Device/WriteFloat32Value","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float32","valueType":"Float32"},{"resourceName":"EnableRandomization_Float32","valueType":"Bool"}]},{"name":"WriteFloat64Value","set":true,"path":"/api/v3/device/name/Random-Float-Device/WriteFloat64Value","url":"http://edgex-core-command:59882","parameters":[{"resourceName":"Float64","valueType":"Float64"},{"resourceName":"EnableRandomization_Float64","valueType":"Bool"}]}]}}` + DeviceCommandResp = `{"apiVersion":"v3","statusCode":200,"event":{"apiVersion":"v3","id":"095090e4-de39-45a1-a0fa-18bc340104e6","deviceName":"Random-Float-Device","profileName":"Random-Float-Device","sourceName":"Float32","origin":1661851070562067780,"readings":[{"id":"972bf6be-3b01-49fc-b211-a43ed51d207d","origin":1661851070562067780,"deviceName":"Random-Float-Device","resourceName":"Float32","profileName":"Random-Float-Device","valueType":"Float32","value":"-2.038811e+38"}]}}` + + DeviceUpdateSuccess = `[{"apiVersion":"v3","statusCode":200}] ` + + DeviceUpdateProperty = `{"apiVersion":"v3","statusCode":200}` +) + +var deviceClient = NewEdgexDeviceClient("edgex-core-metadata:59881", "edgex-core-command:59882") + +func Test_Get(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v3/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceMetadata)) + + device, err := deviceClient.Get(context.TODO(), "Random-Float-Device", clients.GetOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, "Random-Float-Device", device.Spec.Profile) +} + +func Test_List(t *testing.T) { + + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v3/device/all?limit=-1", + httpmock.NewStringResponder(200, DeviceListMetadata)) + + devices, err := deviceClient.List(context.TODO(), clients.ListOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, len(devices), 5) +} + +func Test_Create(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v3/device", + httpmock.NewStringResponder(207, DeviceCreateSuccess)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Name = "test-Random-Float-Device" + + create, err := deviceClient.Create(context.TODO(), &device, clients.CreateOptions{}) + assert.Nil(t, err) + + assert.Equal(t, "test-Random-Float-Device", create.Name) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v3/device", + httpmock.NewStringResponder(207, DeviceCreateFail)) + + create, err = deviceClient.Create(context.TODO(), &device, clients.CreateOptions{}) + assert.NotNil(t, err) + assert.Nil(t, create) +} + +func Test_Delete(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v3/device/name/test-Random-Float-Device", + httpmock.NewStringResponder(200, DeviceDeleteSuccess)) + + err := deviceClient.Delete(context.TODO(), "test-Random-Float-Device", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v3/device/name/test-Random-Float-Device", + httpmock.NewStringResponder(404, DeviceDeleteFail)) + + err = deviceClient.Delete(context.TODO(), "test-Random-Float-Device", clients.DeleteOptions{}) + assert.NotNil(t, err) +} + +func Test_GetPropertyState(t *testing.T) { + + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v3/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v3/device/name/Random-Float-Device/Float32", + httpmock.NewStringResponder(200, DeviceCommandResp)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + + _, err = deviceClient.GetPropertyState(context.TODO(), "Float32", &device, clients.GetOptions{}) + assert.Nil(t, err) +} + +func Test_ListPropertiesState(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v3/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + + _, _, err = deviceClient.ListPropertiesState(context.TODO(), &device, clients.ListOptions{}) + assert.Nil(t, err) +} + +func Test_UpdateDevice(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v3/device", + httpmock.NewStringResponder(207, DeviceUpdateSuccess)) + + var resp edgex_resp.DeviceResponse + + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Spec.AdminState = "LOCKED" + + _, err = deviceClient.Update(context.TODO(), &device, clients.UpdateOptions{}) + assert.Nil(t, err) +} + +func Test_UpdatePropertyState(t *testing.T) { + httpmock.ActivateNonDefault(deviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-command:59882/api/v3/device/name/Random-Float-Device", + httpmock.NewStringResponder(200, DeviceCoreCommands)) + + httpmock.RegisterResponder("PUT", "http://edgex-core-command:59882/api/v3/device/name/Random-Float-Device/Float32", + httpmock.NewStringResponder(200, DeviceUpdateSuccess)) + var resp edgex_resp.DeviceResponse + err := json.Unmarshal([]byte(DeviceMetadata), &resp) + assert.Nil(t, err) + + device := toKubeDevice(resp.Device, "default") + device.Spec.DeviceProperties = map[string]iotv1alpha1.DesiredPropertyState{ + "Float32": { + Name: "Float32", + DesiredValue: "66.66", + }, + } + + err = deviceClient.UpdatePropertyState(context.TODO(), "Float32", &device, clients.UpdateOptions{}) + assert.Nil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceprofile_client.go b/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceprofile_client.go new file mode 100644 index 00000000000..3e16e0e1c39 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceprofile_client.go @@ -0,0 +1,141 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/go-resty/resty/v2" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + devcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceProfile struct { + *resty.Client + CoreMetaAddr string +} + +func NewEdgexDeviceProfile(coreMetaAddr string) *EdgexDeviceProfile { + return &EdgexDeviceProfile{ + Client: resty.New(), + CoreMetaAddr: coreMetaAddr, + } +} + +// TODO: support label filtering +func getListDeviceProfileURL(address string, opts devcli.ListOptions) (string, error) { + url := fmt.Sprintf("http://%s%s/all?limit=-1", address, DeviceProfilePath) + return url, nil +} + +func (cdc *EdgexDeviceProfile) List(ctx context.Context, opts devcli.ListOptions) ([]v1alpha1.DeviceProfile, error) { + klog.V(5).Info("will list DeviceProfiles") + lp, err := getListDeviceProfileURL(cdc.CoreMetaAddr, opts) + if err != nil { + return nil, err + } + resp, err := cdc.R().EnableTrace().Get(lp) + if err != nil { + return nil, err + } + var mdpResp responses.MultiDeviceProfilesResponse + if err := json.Unmarshal(resp.Body(), &mdpResp); err != nil { + return nil, err + } + var deviceProfiles []v1alpha1.DeviceProfile + for _, dp := range mdpResp.Profiles { + deviceProfiles = append(deviceProfiles, toKubeDeviceProfile(&dp, opts.Namespace)) + } + return deviceProfiles, nil +} + +func (cdc *EdgexDeviceProfile) Get(ctx context.Context, name string, opts devcli.GetOptions) (*v1alpha1.DeviceProfile, error) { + klog.V(5).Infof("will get DeviceProfiles: %s", name) + var dpResp responses.DeviceProfileResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", cdc.CoreMetaAddr, DeviceProfilePath, name) + resp, err := cdc.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("DeviceProfile %s not found", name) + } + if err = json.Unmarshal(resp.Body(), &dpResp); err != nil { + return nil, err + } + kubedp := toKubeDeviceProfile(&dpResp.Profile, opts.Namespace) + return &kubedp, nil +} + +func (cdc *EdgexDeviceProfile) Create(ctx context.Context, deviceProfile *v1alpha1.DeviceProfile, opts devcli.CreateOptions) (*v1alpha1.DeviceProfile, error) { + dps := []*v1alpha1.DeviceProfile{deviceProfile} + req := makeEdgeXDeviceProfilesRequest(dps) + klog.V(5).Infof("will add the DeviceProfile: %s", deviceProfile.Name) + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postURL := fmt.Sprintf("http://%s%s", cdc.CoreMetaAddr, DeviceProfilePath) + resp, err := cdc.R().SetBody(reqBody).Post(postURL) + if err != nil { + return nil, err + } + if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create edgex deviceProfile err: %s", string(resp.Body())) // 假定 resp.Body() 存了 msg 信息 + } + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDeviceProfile := deviceProfile.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDeviceProfile.Status.EdgeId = edgexResps[0].Id + createdDeviceProfile.Status.Synced = true + } else { + return nil, fmt.Errorf("create deviceprofile on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch DeviceProfile count, the response is : %s", resp.Body()) + } + return createdDeviceProfile, err +} + +// TODO: edgex does not support update DeviceProfile +func (cdc *EdgexDeviceProfile) Update(ctx context.Context, deviceProfile *v1alpha1.DeviceProfile, opts devcli.UpdateOptions) (*v1alpha1.DeviceProfile, error) { + return nil, nil +} + +func (cdc *EdgexDeviceProfile) Delete(ctx context.Context, name string, opts devcli.DeleteOptions) error { + klog.V(5).Infof("will delete the DeviceProfile: %s", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", cdc.CoreMetaAddr, DeviceProfilePath, name) + resp, err := cdc.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return fmt.Errorf("delete edgex deviceProfile err: %s", string(resp.Body())) // 假定 resp.Body() 存了 msg 信息 + } + return nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceprofile_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceprofile_client_test.go new file mode 100644 index 00000000000..c0b3c4babcb --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceprofile_client_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v3 + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceProfileListMetaData = `{"apiVersion":"v3","statusCode":200,"totalCount":5,"profiles":[{"created":1661829206499,"modified":1661829206499,"id":"cf624c1f-c93a-48c0-b327-b00c7dc171f1","name":"Random-Binary-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"Generate random binary value","name":"Binary","isHidden":false,"tag":"","properties":{"valueType":"Binary","readWrite":"R","units":"","defaultValue":"","assertion":"","mediaType":"random"},"attributes":null}],"deviceCommands":[]},{"created":1661829206501,"modified":1661829206501,"id":"adeafefa-2d11-4eee-8fe9-a4742f85f7fb","name":"Random-UnsignedInteger-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint8","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint16","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint8 value","name":"Uint8","isHidden":false,"tag":"","properties":{"valueType":"Uint8","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint16 value","name":"Uint16","isHidden":false,"tag":"","properties":{"valueType":"Uint16","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint32 value","name":"Uint32","isHidden":false,"tag":"","properties":{"valueType":"Uint32","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint64 value","name":"Uint64","isHidden":false,"tag":"","properties":{"valueType":"Uint64","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint8Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint16Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Uint64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint8 array value","name":"Uint8Array","isHidden":false,"tag":"","properties":{"valueType":"Uint8Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint16 array value","name":"Uint16Array","isHidden":false,"tag":"","properties":{"valueType":"Uint16Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint32 array value","name":"Uint32Array","isHidden":false,"tag":"","properties":{"valueType":"Uint32Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random uint64 array value","name":"Uint64Array","isHidden":false,"tag":"","properties":{"valueType":"Uint64Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteUint8Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint8","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint8","defaultValue":"false","mappings":null}]},{"name":"WriteUint16Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint16","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint16","defaultValue":"false","mappings":null}]},{"name":"WriteUint32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint32","defaultValue":"false","mappings":null}]},{"name":"WriteUint64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint64","defaultValue":"false","mappings":null}]},{"name":"WriteUint8ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint8Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint8Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint16ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint16Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint16Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint32Array","defaultValue":"false","mappings":null}]},{"name":"WriteUint64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Uint64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Uint64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206500,"modified":1661829206500,"id":"67f4a5a1-06e6-4051-b71d-655ec5dd4eb2","name":"Random-Integer-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int8","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int16","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int8 value","name":"Int8","isHidden":false,"tag":"","properties":{"valueType":"Int8","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int16 value","name":"Int16","isHidden":false,"tag":"","properties":{"valueType":"Int16","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int32 value","name":"Int32","isHidden":false,"tag":"","properties":{"valueType":"Int32","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int64 value","name":"Int64","isHidden":false,"tag":"","properties":{"valueType":"Int64","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int8Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int16Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Int64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int8 array value","name":"Int8Array","isHidden":false,"tag":"","properties":{"valueType":"Int8Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int16 array value","name":"Int16Array","isHidden":false,"tag":"","properties":{"valueType":"Int16Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int32 array value","name":"Int32Array","isHidden":false,"tag":"","properties":{"valueType":"Int32Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random int64 array value","name":"Int64Array","isHidden":false,"tag":"","properties":{"valueType":"Int64Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteInt8Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int8","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int8","defaultValue":"false","mappings":null}]},{"name":"WriteInt16Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int16","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int16","defaultValue":"false","mappings":null}]},{"name":"WriteInt32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int32","defaultValue":"false","mappings":null}]},{"name":"WriteInt64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int64","defaultValue":"false","mappings":null}]},{"name":"WriteInt8ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int8Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int8Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt16ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int16Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int16Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int32Array","defaultValue":"false","mappings":null}]},{"name":"WriteInt64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Int64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Int64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206500,"modified":1661829206500,"id":"30b8448f-0532-44fb-aed7-5fe4bca16f9a","name":"Random-Float-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float32","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float64","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float32 value","name":"Float32","isHidden":false,"tag":"","properties":{"valueType":"Float32","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float64 value","name":"Float64","isHidden":false,"tag":"","properties":{"valueType":"Float64","readWrite":"RW","units":"","defaultValue":"0","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float32Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Float64Array","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float32 array value","name":"Float32Array","isHidden":false,"tag":"","properties":{"valueType":"Float32Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random float64 array value","name":"Float64Array","isHidden":false,"tag":"","properties":{"valueType":"Float64Array","readWrite":"RW","units":"","defaultValue":"[0]","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteFloat32Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float32","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float32","defaultValue":"false","mappings":null}]},{"name":"WriteFloat64Value","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float64","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float64","defaultValue":"false","mappings":null}]},{"name":"WriteFloat32ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float32Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float32Array","defaultValue":"false","mappings":null}]},{"name":"WriteFloat64ArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Float64Array","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Float64Array","defaultValue":"false","mappings":null}]}]},{"created":1661829206499,"modified":1661829206499,"id":"01dfe04d-f361-41fd-b1c4-7ca0718f461a","name":"Random-Boolean-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Bool","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean value","name":"Bool","isHidden":false,"tag":"","properties":{"valueType":"Bool","readWrite":"RW","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_BoolArray","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean array value","name":"BoolArray","isHidden":false,"tag":"","properties":{"valueType":"BoolArray","readWrite":"RW","units":"","defaultValue":"[true]","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteBoolValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Bool","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Bool","defaultValue":"false","mappings":null}]},{"name":"WriteBoolArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"BoolArray","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_BoolArray","defaultValue":"false","mappings":null}]}]}]}` + DeviceProfileMetaData = `{"apiVersion":"v3","statusCode":200,"profile":{"created":1661829206499,"modified":1661829206499,"id":"01dfe04d-f361-41fd-b1c4-7ca0718f461a","name":"Random-Boolean-Device","manufacturer":"IOTech","description":"Example of Device-Virtual","model":"Device-Virtual-01","labels":["device-virtual-example"],"deviceResources":[{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_Bool","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean value","name":"Bool","isHidden":false,"tag":"","properties":{"valueType":"Bool","readWrite":"RW","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"used to decide whether to re-generate a random value","name":"EnableRandomization_BoolArray","isHidden":true,"tag":"","properties":{"valueType":"Bool","readWrite":"W","units":"","defaultValue":"true","assertion":"","mediaType":""},"attributes":null},{"description":"Generate random boolean array value","name":"BoolArray","isHidden":false,"tag":"","properties":{"valueType":"BoolArray","readWrite":"RW","units":"","defaultValue":"[true]","assertion":"","mediaType":""},"attributes":null}],"deviceCommands":[{"name":"WriteBoolValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"Bool","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_Bool","defaultValue":"false","mappings":null}]},{"name":"WriteBoolArrayValue","isHidden":false,"readWrite":"W","resourceOperations":[{"deviceResource":"BoolArray","defaultValue":"","mappings":null},{"deviceResource":"EnableRandomization_BoolArray","defaultValue":"false","mappings":null}]}]}}` + + ProfileCreateSuccess = `[{"apiVersion":"v3","statusCode":201,"id":"a583b97d-7c4d-4b7c-8b93-51da9e68518c"}]` + ProfileCreateFail = `[{"apiVersion":"v3","message":"device profile name test-Random-Boolean-Device exists","statusCode":409}]` + + ProfileDeleteSuccess = `{"apiVersion":"v3","statusCode":200}` + ProfileDeleteFail = `{"apiVersion":"v3","message":"fail to delete the device profile with name test-Random-Boolean-Device","statusCode":404}` +) + +var profileClient = NewEdgexDeviceProfile("edgex-core-metadata:59881") + +func Test_ListProfile(t *testing.T) { + + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v3/deviceprofile/all?limit=-1", + httpmock.NewStringResponder(200, DeviceProfileListMetaData)) + profiles, err := profileClient.List(context.TODO(), clients.ListOptions{Namespace: "default"}) + assert.Nil(t, err) + + assert.Equal(t, 5, len(profiles)) +} + +func Test_GetProfile(T *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v3/deviceprofile/name/Random-Boolean-Device", + httpmock.NewStringResponder(200, DeviceProfileMetaData)) + + _, err := profileClient.Get(context.TODO(), "Random-Boolean-Device", clients.GetOptions{Namespace: "default"}) + assert.Nil(T, err) +} + +func Test_CreateProfile(t *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v3/deviceprofile", + httpmock.NewStringResponder(207, ProfileCreateSuccess)) + + var resp edgex_resp.DeviceProfileResponse + + err := json.Unmarshal([]byte(DeviceProfileMetaData), &resp) + assert.Nil(t, err) + + profile := toKubeDeviceProfile(&resp.Profile, "default") + profile.Name = "test-Random-Boolean-Device" + + _, err = profileClient.Create(context.TODO(), &profile, clients.CreateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v3/deviceprofile", + httpmock.NewStringResponder(207, ProfileCreateFail)) + + _, err = profileClient.Create(context.TODO(), &profile, clients.CreateOptions{}) + assert.NotNil(t, err) +} + +func Test_DeleteProfile(t *testing.T) { + httpmock.ActivateNonDefault(profileClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v3/deviceprofile/name/test-Random-Boolean-Device", + httpmock.NewStringResponder(200, ProfileDeleteSuccess)) + + err := profileClient.Delete(context.TODO(), "test-Random-Boolean-Device", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v3/deviceprofile/name/test-Random-Boolean-Device", + httpmock.NewStringResponder(404, ProfileDeleteFail)) + + err = profileClient.Delete(context.TODO(), "test-Random-Boolean-Device", clients.DeleteOptions{}) + assert.NotNil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceservice_client.go b/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceservice_client.go new file mode 100644 index 00000000000..b6bf060494c --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceservice_client.go @@ -0,0 +1,166 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/go-resty/resty/v2" + "k8s.io/klog/v2" + + "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + edgeCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +type EdgexDeviceServiceClient struct { + *resty.Client + CoreMetaAddr string +} + +func NewEdgexDeviceServiceClient(coreMetaAddr string) *EdgexDeviceServiceClient { + return &EdgexDeviceServiceClient{ + Client: resty.New(), + CoreMetaAddr: coreMetaAddr, + } +} + +// Create function sends a POST request to EdgeX to add a new deviceService +func (eds *EdgexDeviceServiceClient) Create(ctx context.Context, deviceService *v1alpha1.DeviceService, options edgeCli.CreateOptions) (*v1alpha1.DeviceService, error) { + dss := []*v1alpha1.DeviceService{deviceService} + req := makeEdgeXDeviceService(dss) + klog.V(5).InfoS("will add the DeviceServices", "DeviceService", deviceService.Name) + jsonBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + postPath := fmt.Sprintf("http://%s%s", eds.CoreMetaAddr, DeviceServicePath) + resp, err := eds.R(). + SetBody(jsonBody).Post(postPath) + if err != nil { + return nil, err + } else if resp.StatusCode() != http.StatusMultiStatus { + return nil, fmt.Errorf("create DeviceService on edgex foundry failed, the response is : %s", resp.Body()) + } + + var edgexResps []*common.BaseWithIdResponse + if err = json.Unmarshal(resp.Body(), &edgexResps); err != nil { + return nil, err + } + createdDeviceService := deviceService.DeepCopy() + if len(edgexResps) == 1 { + if edgexResps[0].StatusCode == http.StatusCreated { + createdDeviceService.Status.EdgeId = edgexResps[0].Id + createdDeviceService.Status.Synced = true + } else { + return nil, fmt.Errorf("create DeviceService on edgex foundry failed, the response is : %s", resp.Body()) + } + } else { + return nil, fmt.Errorf("edgex BaseWithIdResponse count mismatch DeviceService count, the response is : %s", resp.Body()) + } + return createdDeviceService, err +} + +// Delete function sends a request to EdgeX to delete a deviceService +func (eds *EdgexDeviceServiceClient) Delete(ctx context.Context, name string, option edgeCli.DeleteOptions) error { + klog.V(5).InfoS("will delete the DeviceService", "DeviceService", name) + delURL := fmt.Sprintf("http://%s%s/name/%s", eds.CoreMetaAddr, DeviceServicePath, name) + resp, err := eds.R().Delete(delURL) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return fmt.Errorf("delete edgex deviceservice err: %s", string(resp.Body())) + } + return nil +} + +// Update is used to set the admin or operating state of the deviceService by unique name of the deviceService. +// TODO support to update other fields +func (eds *EdgexDeviceServiceClient) Update(ctx context.Context, ds *v1alpha1.DeviceService, options edgeCli.UpdateOptions) (*v1alpha1.DeviceService, error) { + patchURL := fmt.Sprintf("http://%s%s", eds.CoreMetaAddr, DeviceServicePath) + if ds == nil { + return nil, nil + } + + if ds.Status.EdgeId == "" { + return nil, fmt.Errorf("failed to update deviceservice %s with empty edgex id", ds.Name) + } + edgeDs := toEdgexDeviceService(ds) + edgeDs.Id = ds.Status.EdgeId + dsJson, err := json.Marshal(&edgeDs) + if err != nil { + return nil, err + } + resp, err := eds.R(). + SetBody(dsJson).Patch(patchURL) + if err != nil { + return nil, err + } + + if resp.StatusCode() == http.StatusOK || resp.StatusCode() == http.StatusMultiStatus { + return ds, nil + } else { + return nil, fmt.Errorf("request to patch deviceservice failed, errcode:%d", resp.StatusCode()) + } +} + +// Get is used to query the deviceService information corresponding to the deviceService name +func (eds *EdgexDeviceServiceClient) Get(ctx context.Context, name string, options edgeCli.GetOptions) (*v1alpha1.DeviceService, error) { + klog.V(5).InfoS("will get DeviceServices", "DeviceService", name) + var dsResp responses.DeviceServiceResponse + getURL := fmt.Sprintf("http://%s%s/name/%s", eds.CoreMetaAddr, DeviceServicePath, name) + resp, err := eds.R().Get(getURL) + if err != nil { + return nil, err + } + if resp.StatusCode() == http.StatusNotFound { + return nil, fmt.Errorf("deviceservice %s not found", name) + } + err = json.Unmarshal(resp.Body(), &dsResp) + if err != nil { + return nil, err + } + ds := toKubeDeviceService(dsResp.Service, options.Namespace) + return &ds, nil +} + +// List is used to get all deviceService objects on edge platform +// The Hanoi version currently supports only a single label and does not support other filters +func (eds *EdgexDeviceServiceClient) List(ctx context.Context, options edgeCli.ListOptions) ([]v1alpha1.DeviceService, error) { + klog.V(5).Info("will list DeviceServices") + lp := fmt.Sprintf("http://%s%s/all?limit=-1", eds.CoreMetaAddr, DeviceServicePath) + resp, err := eds.R(). + EnableTrace(). + Get(lp) + if err != nil { + return nil, err + } + var mdsResponse responses.MultiDeviceServicesResponse + if err := json.Unmarshal(resp.Body(), &mdsResponse); err != nil { + return nil, err + } + var res []v1alpha1.DeviceService + for _, ds := range mdsResponse.Services { + res = append(res, toKubeDeviceService(ds, options.Namespace)) + } + return res, nil +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceservice_client_test.go b/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceservice_client_test.go new file mode 100644 index 00000000000..0fa72a094fe --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/v3/deviceservice_client_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v3 + +import ( + "context" + "encoding/json" + "testing" + + edgex_resp "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" +) + +const ( + DeviceServiceListMetaData = `{"apiVersion":"v3","statusCode":200,"totalCount":1,"services":[{"created":1661829206490,"modified":1661850999190,"id":"74516e96-973d-4cad-bad1-afd4b3a8ea46","name":"device-virtual","baseAddress":"http://edgex-device-virtual:59900","adminState":"UNLOCKED"}]}` + DeviceServiceMetaData = `{"apiVersion":"v3","statusCode":200,"service":{"created":1661829206490,"modified":1661850999190,"id":"74516e96-973d-4cad-bad1-afd4b3a8ea46","name":"device-virtual","baseAddress":"http://edgex-device-virtual:59900","adminState":"UNLOCKED"}}` + ServiceCreateSuccess = `[{"apiVersion":"v3","statusCode":201,"id":"a583b97d-7c4d-4b7c-8b93-51da9e68518c"}]` + ServiceCreateFail = `[{"apiVersion":"v3","message":"device service name test-device-virtual exists","statusCode":409}]` + + ServiceDeleteSuccess = `{"apiVersion":"v3","statusCode":200}` + ServiceDeleteFail = `{"apiVersion":"v3","message":"fail to delete the device profile with name test-Random-Boolean-Device","statusCode":404}` + + ServiceUpdateSuccess = `[{"apiVersion":"v3","statusCode":200}]` + ServiceUpdateFail = `[{"apiVersion":"v3","message":"fail to query object *models.DeviceService, because id: md|ds:01dfe04d-f361-41fd-b1c4-7ca0718f461a doesn't exist in the database","statusCode":404}]` +) + +var serviceClient = NewEdgexDeviceServiceClient("edgex-core-metadata:59881") + +func Test_GetService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v3/deviceservice/name/device-virtual", + httpmock.NewStringResponder(200, DeviceServiceMetaData)) + + _, err := serviceClient.Get(context.TODO(), "device-virtual", clients.GetOptions{Namespace: "default"}) + assert.Nil(t, err) +} + +func Test_ListService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", "http://edgex-core-metadata:59881/api/v3/deviceservice/all?limit=-1", + httpmock.NewStringResponder(200, DeviceServiceListMetaData)) + + services, err := serviceClient.List(context.TODO(), clients.ListOptions{}) + assert.Nil(t, err) + assert.Equal(t, 1, len(services)) +} + +func Test_CreateService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v3/deviceservice", + httpmock.NewStringResponder(207, ServiceCreateSuccess)) + + var resp edgex_resp.DeviceServiceResponse + + err := json.Unmarshal([]byte(DeviceServiceMetaData), &resp) + assert.Nil(t, err) + + service := toKubeDeviceService(resp.Service, "default") + service.Name = "test-device-virtual" + + _, err = serviceClient.Create(context.TODO(), &service, clients.CreateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("POST", "http://edgex-core-metadata:59881/api/v3/deviceservice", + httpmock.NewStringResponder(207, ServiceCreateFail)) +} + +func Test_DeleteService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v3/deviceservice/name/test-device-virtual", + httpmock.NewStringResponder(200, ServiceDeleteSuccess)) + + err := serviceClient.Delete(context.TODO(), "test-device-virtual", clients.DeleteOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("DELETE", "http://edgex-core-metadata:59881/api/v3/deviceservice/name/test-device-virtual", + httpmock.NewStringResponder(404, ServiceDeleteFail)) + + err = serviceClient.Delete(context.TODO(), "test-device-virtual", clients.DeleteOptions{}) + assert.NotNil(t, err) +} + +func Test_UpdateService(t *testing.T) { + httpmock.ActivateNonDefault(serviceClient.Client.GetClient()) + defer httpmock.DeactivateAndReset() + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v3/deviceservice", + httpmock.NewStringResponder(200, ServiceUpdateSuccess)) + var resp edgex_resp.DeviceServiceResponse + + err := json.Unmarshal([]byte(DeviceServiceMetaData), &resp) + assert.Nil(t, err) + + service := toKubeDeviceService(resp.Service, "default") + _, err = serviceClient.Update(context.TODO(), &service, clients.UpdateOptions{}) + assert.Nil(t, err) + + httpmock.RegisterResponder("PATCH", "http://edgex-core-metadata:59881/api/v3/deviceservice", + httpmock.NewStringResponder(404, ServiceUpdateFail)) + + _, err = serviceClient.Update(context.TODO(), &service, clients.UpdateOptions{}) + assert.NotNil(t, err) +} diff --git a/pkg/yurtiotdock/clients/edgex-foundry/v3/util.go b/pkg/yurtiotdock/clients/edgex-foundry/v3/util.go new file mode 100644 index 00000000000..60c4fd05662 --- /dev/null +++ b/pkg/yurtiotdock/clients/edgex-foundry/v3/util.go @@ -0,0 +1,472 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import ( + "fmt" + "strings" + + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common" + "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests" + "github.com/edgexfoundry/go-mod-core-contracts/v3/models" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" + util "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" +) + +const ( + EdgeXObjectName = "yurt-iot-dock/edgex-object.name" + DeviceServicePath = "/api/v3/deviceservice" + DeviceProfilePath = "/api/v3/deviceprofile" + DevicePath = "/api/v3/device" + CommandResponsePath = "/api/v3/device" + + APIVersionV3 = "v3" +) + +type ClientURL struct { + Host string + Port int +} + +func getEdgeXName(provider metav1.Object) string { + var actualDeviceName string + if _, ok := provider.GetLabels()[EdgeXObjectName]; ok { + actualDeviceName = provider.GetLabels()[EdgeXObjectName] + } else { + actualDeviceName = provider.GetName() + } + return actualDeviceName +} + +func toEdgexDeviceService(ds *iotv1alpha1.DeviceService) dtos.DeviceService { + return dtos.DeviceService{ + Description: ds.Spec.Description, + Name: getEdgeXName(ds), + Labels: ds.Spec.Labels, + AdminState: string(ds.Spec.AdminState), + BaseAddress: ds.Spec.BaseAddress, + // TODO: Metric LastConnected / LastReported + } +} + +func toEdgeXDeviceResourceSlice(drs []iotv1alpha1.DeviceResource) []dtos.DeviceResource { + var ret []dtos.DeviceResource + for _, dr := range drs { + ret = append(ret, toEdgeXDeviceResource(dr)) + } + return ret +} + +func toEdgeXDeviceResource(dr iotv1alpha1.DeviceResource) dtos.DeviceResource { + genericAttrs := make(map[string]interface{}) + for k, v := range dr.Attributes { + genericAttrs[k] = v + } + + return dtos.DeviceResource{ + Description: dr.Description, + Name: dr.Name, + // Tag: dr.Tag, + Properties: toEdgeXProfileProperty(dr.Properties), + Attributes: genericAttrs, + } +} + +func toEdgeXProfileProperty(pp iotv1alpha1.ResourceProperties) dtos.ResourceProperties { + return dtos.ResourceProperties{ + ReadWrite: pp.ReadWrite, + Minimum: util.StrToFloat(pp.Minimum), + Maximum: util.StrToFloat(pp.Maximum), + DefaultValue: pp.DefaultValue, + Mask: util.StrToUint(pp.Mask), + Shift: util.StrToInt(pp.Shift), + Scale: util.StrToFloat(pp.Scale), + Offset: util.StrToFloat(pp.Offset), + Base: util.StrToFloat(pp.Base), + Assertion: pp.Assertion, + MediaType: pp.MediaType, + Units: pp.Units, + ValueType: pp.ValueType, + } +} + +func toKubeDeviceService(ds dtos.DeviceService, namespace string) iotv1alpha1.DeviceService { + return iotv1alpha1.DeviceService{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(ds.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: ds.Name, + }, + }, + Spec: iotv1alpha1.DeviceServiceSpec{ + Description: ds.Description, + Labels: ds.Labels, + AdminState: iotv1alpha1.AdminState(ds.AdminState), + BaseAddress: ds.BaseAddress, + }, + Status: iotv1alpha1.DeviceServiceStatus{ + EdgeId: ds.Id, + AdminState: iotv1alpha1.AdminState(ds.AdminState), + // TODO: Metric LastConnected / LastReported + }, + } +} + +func toEdgeXDevice(d *iotv1alpha1.Device) dtos.Device { + md := dtos.Device{ + Description: d.Spec.Description, + Name: getEdgeXName(d), + AdminState: string(toEdgeXAdminState(d.Spec.AdminState)), + OperatingState: string(toEdgeXOperatingState(d.Spec.OperatingState)), + Protocols: toEdgeXProtocols(d.Spec.Protocols), + // TODO: Metric LastConnected / LastReported + Labels: d.Spec.Labels, + Location: d.Spec.Location, + ServiceName: d.Spec.Service, + ProfileName: d.Spec.Profile, + } + if d.Status.EdgeId != "" { + md.Id = d.Status.EdgeId + } + return md +} + +func toEdgeXUpdateDevice(d *iotv1alpha1.Device) dtos.UpdateDevice { + adminState := string(toEdgeXAdminState(d.Spec.AdminState)) + operationState := string(toEdgeXOperatingState(d.Spec.OperatingState)) + md := dtos.UpdateDevice{ + Description: &d.Spec.Description, + Name: &d.Name, + AdminState: &adminState, + OperatingState: &operationState, + Protocols: toEdgeXProtocols(d.Spec.Protocols), + Labels: d.Spec.Labels, + Location: d.Spec.Location, + ServiceName: &d.Spec.Service, + ProfileName: &d.Spec.Profile, + // TODO: Metric LastConnected / LastReported + } + if d.Status.EdgeId != "" { + md.Id = &d.Status.EdgeId + } + return md +} + +func toEdgeXProtocols( + pps map[string]iotv1alpha1.ProtocolProperties) map[string]dtos.ProtocolProperties { + ret := make(map[string]dtos.ProtocolProperties, len(pps)) + for k, v := range pps { + propMap := make(map[string]interface{}) + for key, value := range v { + propMap[key] = value + } + ret[k] = dtos.ProtocolProperties(propMap) + } + return ret +} + +func toEdgeXAdminState(as iotv1alpha1.AdminState) models.AdminState { + if as == iotv1alpha1.Locked { + return models.Locked + } + return models.Unlocked +} + +func toEdgeXOperatingState(os iotv1alpha1.OperatingState) models.OperatingState { + if os == iotv1alpha1.Up { + return models.Up + } else if os == iotv1alpha1.Down { + return models.Down + } + return models.Unknown +} + +// toKubeDevice serialize the EdgeX Device to the corresponding Kubernetes Device +func toKubeDevice(ed dtos.Device, namespace string) iotv1alpha1.Device { + var loc string + if ed.Location != nil { + loc = ed.Location.(string) + } + return iotv1alpha1.Device{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(ed.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: ed.Name, + }, + }, + Spec: iotv1alpha1.DeviceSpec{ + Description: ed.Description, + AdminState: iotv1alpha1.AdminState(ed.AdminState), + OperatingState: iotv1alpha1.OperatingState(ed.OperatingState), + Protocols: toKubeProtocols(ed.Protocols), + Labels: ed.Labels, + Location: loc, + Service: ed.ServiceName, + Profile: ed.ProfileName, + // TODO: Notify + }, + Status: iotv1alpha1.DeviceStatus{ + // TODO: Metric LastConnected / LastReported + Synced: true, + EdgeId: ed.Id, + AdminState: iotv1alpha1.AdminState(ed.AdminState), + OperatingState: iotv1alpha1.OperatingState(ed.OperatingState), + }, + } +} + +// toKubeProtocols serialize the EdgeX ProtocolProperties to the corresponding +// Kubernetes OperatingState +func toKubeProtocols( + eps map[string]dtos.ProtocolProperties) map[string]iotv1alpha1.ProtocolProperties { + ret := map[string]iotv1alpha1.ProtocolProperties{} + for k, v := range eps { + propMap := make(map[string]string) + for key, value := range v { + switch asserted := value.(type) { + case string: + propMap[key] = asserted + continue + case int: + propMap[key] = fmt.Sprintf("%d", asserted) + continue + case float64: + propMap[key] = fmt.Sprintf("%f", asserted) + continue + case fmt.Stringer: + propMap[key] = asserted.String() + continue + } + } + ret[k] = iotv1alpha1.ProtocolProperties(propMap) + } + return ret +} + +// toKubeDeviceProfile create DeviceProfile in cloud according to devicProfile in edge +func toKubeDeviceProfile(dp *dtos.DeviceProfile, namespace string) iotv1alpha1.DeviceProfile { + return iotv1alpha1.DeviceProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: toKubeName(dp.Name), + Namespace: namespace, + Labels: map[string]string{ + EdgeXObjectName: dp.Name, + }, + }, + Spec: iotv1alpha1.DeviceProfileSpec{ + Description: dp.Description, + Manufacturer: dp.Manufacturer, + Model: dp.Model, + Labels: dp.Labels, + DeviceResources: toKubeDeviceResources(dp.DeviceResources), + DeviceCommands: toKubeDeviceCommand(dp.DeviceCommands), + }, + Status: iotv1alpha1.DeviceProfileStatus{ + EdgeId: dp.Id, + Synced: true, + }, + } +} + +func toKubeDeviceCommand(dcs []dtos.DeviceCommand) []iotv1alpha1.DeviceCommand { + var ret []iotv1alpha1.DeviceCommand + for _, dc := range dcs { + ret = append(ret, iotv1alpha1.DeviceCommand{ + Name: dc.Name, + ReadWrite: dc.ReadWrite, + IsHidden: dc.IsHidden, + ResourceOperations: toKubeResourceOperations(dc.ResourceOperations), + }) + } + return ret +} + +func toEdgeXDeviceCommand(dcs []iotv1alpha1.DeviceCommand) []dtos.DeviceCommand { + var ret []dtos.DeviceCommand + for _, dc := range dcs { + ret = append(ret, dtos.DeviceCommand{ + Name: dc.Name, + ReadWrite: dc.ReadWrite, + IsHidden: dc.IsHidden, + ResourceOperations: toEdgeXResourceOperations(dc.ResourceOperations), + }) + } + return ret +} + +func toKubeResourceOperations(ros []dtos.ResourceOperation) []iotv1alpha1.ResourceOperation { + var ret []iotv1alpha1.ResourceOperation + for _, ro := range ros { + ret = append(ret, iotv1alpha1.ResourceOperation{ + DeviceResource: ro.DeviceResource, + Mappings: ro.Mappings, + DefaultValue: ro.DefaultValue, + }) + } + return ret +} + +func toEdgeXResourceOperations(ros []iotv1alpha1.ResourceOperation) []dtos.ResourceOperation { + var ret []dtos.ResourceOperation + for _, ro := range ros { + ret = append(ret, dtos.ResourceOperation{ + DeviceResource: ro.DeviceResource, + Mappings: ro.Mappings, + DefaultValue: ro.DefaultValue, + }) + } + return ret +} + +func toKubeDeviceResources(drs []dtos.DeviceResource) []iotv1alpha1.DeviceResource { + var ret []iotv1alpha1.DeviceResource + for _, dr := range drs { + ret = append(ret, toKubeDeviceResource(dr)) + } + return ret +} + +func toKubeDeviceResource(dr dtos.DeviceResource) iotv1alpha1.DeviceResource { + concreteAttrs := make(map[string]string) + for k, v := range dr.Attributes { + switch asserted := v.(type) { + case string: + concreteAttrs[k] = asserted + continue + case int: + concreteAttrs[k] = fmt.Sprintf("%d", asserted) + continue + case float64: + concreteAttrs[k] = fmt.Sprintf("%f", asserted) + continue + case fmt.Stringer: + concreteAttrs[k] = asserted.String() + continue + } + } + + return iotv1alpha1.DeviceResource{ + Description: dr.Description, + Name: dr.Name, + // Tag: dr.Tag, + IsHidden: dr.IsHidden, + Properties: toKubeProfileProperty(dr.Properties), + Attributes: concreteAttrs, + } +} + +func toKubeProfileProperty(rp dtos.ResourceProperties) iotv1alpha1.ResourceProperties { + return iotv1alpha1.ResourceProperties{ + ValueType: rp.ValueType, + ReadWrite: rp.ReadWrite, + Minimum: util.FloatToStr(rp.Minimum), + Maximum: util.FloatToStr(rp.Maximum), + DefaultValue: rp.DefaultValue, + Mask: util.UintToStr(rp.Mask), + Shift: util.IntToStr(rp.Shift), + Scale: util.FloatToStr(rp.Scale), + Offset: util.FloatToStr(rp.Offset), + Base: util.FloatToStr(rp.Base), + Assertion: rp.Assertion, + MediaType: rp.MediaType, + Units: rp.Units, + } +} + +// toEdgeXDeviceProfile create DeviceProfile in edge according to devicProfile in cloud +func toEdgeXDeviceProfile(dp *iotv1alpha1.DeviceProfile) dtos.DeviceProfile { + return dtos.DeviceProfile{ + DeviceProfileBasicInfo: dtos.DeviceProfileBasicInfo{ + Description: dp.Spec.Description, + Name: getEdgeXName(dp), + Manufacturer: dp.Spec.Manufacturer, + Model: dp.Spec.Model, + Labels: dp.Spec.Labels, + }, + DeviceResources: toEdgeXDeviceResourceSlice(dp.Spec.DeviceResources), + DeviceCommands: toEdgeXDeviceCommand(dp.Spec.DeviceCommands), + } +} + +func makeEdgeXDeviceProfilesRequest(dps []*iotv1alpha1.DeviceProfile) []*requests.DeviceProfileRequest { + var req []*requests.DeviceProfileRequest + for _, dp := range dps { + req = append(req, &requests.DeviceProfileRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV3, + }, + }, + Profile: toEdgeXDeviceProfile(dp), + }) + } + return req +} + +func makeEdgeXDeviceUpdateRequest(devs []*iotv1alpha1.Device) []*requests.UpdateDeviceRequest { + var req []*requests.UpdateDeviceRequest + for _, dev := range devs { + req = append(req, &requests.UpdateDeviceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV3, + }, + }, + Device: toEdgeXUpdateDevice(dev), + }) + } + return req +} + +func makeEdgeXDeviceRequest(devs []*iotv1alpha1.Device) []*requests.AddDeviceRequest { + var req []*requests.AddDeviceRequest + for _, dev := range devs { + req = append(req, &requests.AddDeviceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV3, + }, + }, + Device: toEdgeXDevice(dev), + }) + } + return req +} + +func makeEdgeXDeviceService(dss []*iotv1alpha1.DeviceService) []*requests.AddDeviceServiceRequest { + var req []*requests.AddDeviceServiceRequest + for _, ds := range dss { + req = append(req, &requests.AddDeviceServiceRequest{ + BaseRequest: common.BaseRequest{ + Versionable: common.Versionable{ + ApiVersion: APIVersionV3, + }, + }, + Service: toEdgexDeviceService(ds), + }) + } + return req +} + +func toKubeName(edgexName string) string { + return strings.ReplaceAll(strings.ToLower(edgexName), "_", "-") +} diff --git a/pkg/yurtiotdock/clients/interface.go b/pkg/yurtiotdock/clients/interface.go index 93ffc3574bd..92f2839d27e 100644 --- a/pkg/yurtiotdock/clients/interface.go +++ b/pkg/yurtiotdock/clients/interface.go @@ -91,3 +91,10 @@ type DeviceProfileInterface interface { Get(ctx context.Context, name string, options GetOptions) (*iotv1alpha1.DeviceProfile, error) List(ctx context.Context, options ListOptions) ([]iotv1alpha1.DeviceProfile, error) } + +// IoTDock defines the interfaces which used to create deviceclient, deviceprofileclient, deviceserviceclient +type IoTDock interface { + CreateDeviceClient() (DeviceInterface, error) + CreateDeviceProfileClient() (DeviceProfileInterface, error) + CreateDeviceServiceClient() (DeviceServiceInterface, error) +} diff --git a/pkg/yurtiotdock/controllers/device_controller.go b/pkg/yurtiotdock/controllers/device_controller.go index 662feb3632d..444759de006 100644 --- a/pkg/yurtiotdock/controllers/device_controller.go +++ b/pkg/yurtiotdock/controllers/device_controller.go @@ -34,7 +34,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" - edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + edgexobj "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" util "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" ) @@ -109,8 +109,12 @@ func (r *DeviceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } // SetupWithManager sets up the controller with the Manager. -func (r *DeviceReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { - r.deviceCli = edgexCli.NewEdgexDeviceClient(opts.CoreMetadataAddr, opts.CoreCommandAddr) +func (r *DeviceReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions, edgexdock *edgexobj.EdgexDock) error { + deviceclient, err := edgexdock.CreateDeviceClient() + if err != nil { + return err + } + r.deviceCli = deviceclient r.NodePool = opts.Nodepool r.Namespace = opts.Namespace diff --git a/pkg/yurtiotdock/controllers/device_syncer.go b/pkg/yurtiotdock/controllers/device_syncer.go index 501ba6101dc..64e30c8dc36 100644 --- a/pkg/yurtiotdock/controllers/device_syncer.go +++ b/pkg/yurtiotdock/controllers/device_syncer.go @@ -29,7 +29,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" edgeCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" - efCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + edgexobj "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" ) @@ -46,10 +46,14 @@ type DeviceSyncer struct { } // NewDeviceSyncer initialize a New DeviceSyncer -func NewDeviceSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceSyncer, error) { +func NewDeviceSyncer(client client.Client, opts *options.YurtIoTDockOptions, edgexdock *edgexobj.EdgexDock) (DeviceSyncer, error) { + devicelient, err := edgexdock.CreateDeviceClient() + if err != nil { + return DeviceSyncer{}, err + } return DeviceSyncer{ syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, - deviceCli: efCli.NewEdgexDeviceClient(opts.CoreMetadataAddr, opts.CoreCommandAddr), + deviceCli: devicelient, Client: client, NodePool: opts.Nodepool, Namespace: opts.Namespace, diff --git a/pkg/yurtiotdock/controllers/deviceprofile_controller.go b/pkg/yurtiotdock/controllers/deviceprofile_controller.go index 459f39156b3..3ea4f321918 100644 --- a/pkg/yurtiotdock/controllers/deviceprofile_controller.go +++ b/pkg/yurtiotdock/controllers/deviceprofile_controller.go @@ -32,7 +32,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" - edgexclis "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + edgexobj "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" ) @@ -87,8 +87,12 @@ func (r *DeviceProfileReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // SetupWithManager sets up the controller with the Manager. -func (r *DeviceProfileReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { - r.edgeClient = edgexclis.NewEdgexDeviceProfile(opts.CoreMetadataAddr) +func (r *DeviceProfileReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions, edgexdock *edgexobj.EdgexDock) error { + deviceprofileclient, err := edgexdock.CreateDeviceProfileClient() + if err != nil { + return err + } + r.edgeClient = deviceprofileclient r.NodePool = opts.Nodepool r.Namespace = opts.Namespace diff --git a/pkg/yurtiotdock/controllers/deviceprofile_syncer.go b/pkg/yurtiotdock/controllers/deviceprofile_syncer.go index 72d67729b47..5ae3390f8e5 100644 --- a/pkg/yurtiotdock/controllers/deviceprofile_syncer.go +++ b/pkg/yurtiotdock/controllers/deviceprofile_syncer.go @@ -29,7 +29,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" devcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" - edgexclis "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + edgexobj "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" ) @@ -45,10 +45,14 @@ type DeviceProfileSyncer struct { } // NewDeviceProfileSyncer initialize a New DeviceProfileSyncer -func NewDeviceProfileSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceProfileSyncer, error) { +func NewDeviceProfileSyncer(client client.Client, opts *options.YurtIoTDockOptions, edgexdock *edgexobj.EdgexDock) (DeviceProfileSyncer, error) { + edgeclient, err := edgexdock.CreateDeviceProfileClient() + if err != nil { + return DeviceProfileSyncer{}, err + } return DeviceProfileSyncer{ syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, - edgeClient: edgexclis.NewEdgexDeviceProfile(opts.CoreMetadataAddr), + edgeClient: edgeclient, Client: client, NodePool: opts.Nodepool, Namespace: opts.Namespace, diff --git a/pkg/yurtiotdock/controllers/deviceservice_controller.go b/pkg/yurtiotdock/controllers/deviceservice_controller.go index b924d789c5c..c277220f564 100644 --- a/pkg/yurtiotdock/controllers/deviceservice_controller.go +++ b/pkg/yurtiotdock/controllers/deviceservice_controller.go @@ -33,7 +33,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" - edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + edgexobj "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" util "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" ) @@ -107,8 +107,12 @@ func (r *DeviceServiceReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // SetupWithManager sets up the controller with the Manager. -func (r *DeviceServiceReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions) error { - r.deviceServiceCli = edgexCli.NewEdgexDeviceServiceClient(opts.CoreMetadataAddr) +func (r *DeviceServiceReconciler) SetupWithManager(mgr ctrl.Manager, opts *options.YurtIoTDockOptions, edgexdock *edgexobj.EdgexDock) error { + deviceserviceclient, err := edgexdock.CreateDeviceServiceClient() + if err != nil { + return err + } + r.deviceServiceCli = deviceserviceclient r.NodePool = opts.Nodepool r.Namespace = opts.Namespace diff --git a/pkg/yurtiotdock/controllers/deviceservice_syncer.go b/pkg/yurtiotdock/controllers/deviceservice_syncer.go index 78949c55069..30a6e59b5c4 100644 --- a/pkg/yurtiotdock/controllers/deviceservice_syncer.go +++ b/pkg/yurtiotdock/controllers/deviceservice_syncer.go @@ -28,7 +28,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-iot-dock/app/options" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" iotcli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients" - edgexCli "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" + edgexobj "github.com/openyurtio/openyurt/pkg/yurtiotdock/clients/edgex-foundry" "github.com/openyurtio/openyurt/pkg/yurtiotdock/controllers/util" ) @@ -42,10 +42,14 @@ type DeviceServiceSyncer struct { Namespace string } -func NewDeviceServiceSyncer(client client.Client, opts *options.YurtIoTDockOptions) (DeviceServiceSyncer, error) { +func NewDeviceServiceSyncer(client client.Client, opts *options.YurtIoTDockOptions, edgexdock *edgexobj.EdgexDock) (DeviceServiceSyncer, error) { + deviceserviceclient, err := edgexdock.CreateDeviceServiceClient() + if err != nil { + return DeviceServiceSyncer{}, err + } return DeviceServiceSyncer{ syncPeriod: time.Duration(opts.EdgeSyncPeriod) * time.Second, - deviceServiceCli: edgexCli.NewEdgexDeviceServiceClient(opts.CoreMetadataAddr), + deviceServiceCli: deviceserviceclient, Client: client, NodePool: opts.Nodepool, Namespace: opts.Namespace, diff --git a/pkg/yurtiotdock/controllers/util/string.go b/pkg/yurtiotdock/controllers/util/string.go index 4e7607a4d56..345002d0f12 100644 --- a/pkg/yurtiotdock/controllers/util/string.go +++ b/pkg/yurtiotdock/controllers/util/string.go @@ -16,6 +16,8 @@ limitations under the License. package util +import "strconv" + // IsInStringLst checks if 'str' is in the 'strLst' func IsInStringLst(strLst []string, str string) bool { if len(strLst) == 0 { @@ -28,3 +30,57 @@ func IsInStringLst(strLst []string, str string) bool { } return false } + +func FloatToStr(num *float64) string { + var res string + if num != nil { + res = strconv.FormatFloat(*num, 'f', -1, 64) + } else { + res = "" + } + return res +} + +func UintToStr(num *uint64) string { + var res string + if num != nil { + res = strconv.FormatUint(*(num), 10) + } else { + res = "" + } + return res +} + +func IntToStr(num *int64) string { + var res string + if num != nil { + res = strconv.FormatInt(*(num), 10) + } else { + res = "" + } + return res +} + +func StrToFloat(str string) *float64 { + num, err := strconv.ParseFloat(str, 64) + if err != nil { + return nil + } + return &num +} + +func StrToUint(str string) *uint64 { + num, err := strconv.ParseUint(str, 10, 64) + if err != nil { + return nil + } + return &num +} + +func StrToInt(str string) *int64 { + num, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return nil + } + return &num +} diff --git a/pkg/yurtmanager/controller/platformadmin/iotdock.go b/pkg/yurtmanager/controller/platformadmin/iotdock.go index a1f98c1cde1..15ffaffd7b9 100644 --- a/pkg/yurtmanager/controller/platformadmin/iotdock.go +++ b/pkg/yurtmanager/controller/platformadmin/iotdock.go @@ -76,6 +76,7 @@ func newYurtIoTDockComponent(platformAdmin *iotv1alpha2.PlatformAdmin, platformA "--metrics-bind-address=127.0.0.1:8080", "--leader-elect=false", fmt.Sprintf("--namespace=%s", ns), + fmt.Sprintf("--version=%s", platformAdmin.Spec.Version), }, LivenessProbe: &corev1.Probe{ InitialDelaySeconds: 15, From 85a749985ad576cdbac792dae59966a93b96f1c2 Mon Sep 17 00:00:00 2001 From: Liang Deng <283304489@qq.com> Date: Tue, 22 Aug 2023 17:14:25 +0800 Subject: [PATCH 83/93] proposal of support raven NAT traversal (#1639) Signed-off-by: Liang Deng <283304489@qq.com> --- docs/img/support-raven-nat-traversal/1.png | Bin 0 -> 51555 bytes docs/img/support-raven-nat-traversal/2.png | Bin 0 -> 1519845 bytes .../20230807-support-raven-nat-traversal.md | 245 ++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 docs/img/support-raven-nat-traversal/1.png create mode 100644 docs/img/support-raven-nat-traversal/2.png create mode 100644 docs/proposals/20230807-support-raven-nat-traversal.md diff --git a/docs/img/support-raven-nat-traversal/1.png b/docs/img/support-raven-nat-traversal/1.png new file mode 100644 index 0000000000000000000000000000000000000000..04eb93c091e0a77e63632177e6ac0422a3c8c0ac GIT binary patch literal 51555 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%0A!(2hMbknb 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

    LFnFc2=-iFi{QYJj|qzX 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 O$4wgh-y_bv`Tqgds~2Pd literal 0 HcmV?d00001 diff --git a/docs/proposals/20230807-support-raven-nat-traversal.md b/docs/proposals/20230807-support-raven-nat-traversal.md new file mode 100644 index 00000000000..97344357ef2 --- /dev/null +++ b/docs/proposals/20230807-support-raven-nat-traversal.md @@ -0,0 +1,245 @@ +--- +title: Support raven NAT traversal +authors: + - "@YTGhost" +reviewers: + - "" +creation-date: 2023-08-07 +last-updated: 2023-08-07 +status: provisional +--- + +# Support raven NAT traversal + +## Table of Contents + +- [Support raven NAT traversal](#Support raven NAT traversal) + - [Table of Contents](#table-of-contents) + - [Summary](#summary) + - [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals/Future Work](#non-goalsfuture-work) + - [Proposal](#proposal) + - [Prerequisite Knowledge](#prerequisite-knowledge) + - [Implementation Summary](#implementation-summary) + - [Implementation Details](#implementation-details) + - [Retrieve Hole Punching Information](#retrieve-hole-punching-information) + - [Gateway API](#gateway-api) + - [Create Edge-To-Edge Tunnel Operation](#create-edge-to-edge-tunnel-operation) + - [Hole Punching Effect](#hole-punching effect) + +## Summary + +In edge computing, edge-to-edge and edge-to-cloud communication are common networking scenarios. In the current implementation of Raven, for edge-to-edge communication, all cross-edge traffic is still forwarded through the cloud-based central end, which brings higher communication latency and bandwidth consumption. Therefore, Raven needs to enhance the processing of cross-edge traffic, make full use of the network capabilities of the edge itself, and create edge-to-edge VPN tunnels as much as possible. This proposal aims to present a solution, to create edge-to-edge VPN tunnels as much as possible through NAT traversal. + +## Motivation + +By constructing edge-to-edge VPN tunnels, it can help avoid the high latency and high bandwidth consumption caused by cloud-based forwarding. + +### Goals + +To enhance the handling of cross-edge traffic, we need to achieve the following goals: + +- The two currently supported VPN backends (WireGuard and Libreswan) should be able to automatically establish edge-to-edge VPN tunnels. +- If an edge-to-edge tunnel cannot be established, forwarding through the cloud-based central end should still be possible. + +### Non-Goals/Future Work + +- Optimize the network topology by shortest path calculation. + +## Proposal + +### Prerequisite Knowledge + +In the STUN (Session Traversal Utilities for NAT) protocol, based on the mapping method from private IP addresses and port numbers to public IP addresses and port numbers, NAT is divided into four types, as shown in the figure below. + +![NAT type in STUN](../img/support-raven-nat-traversal/1.png) + +![Different NAT type combinations and whether or not they are routable.](../img/support-raven-nat-traversal/2.png) + +### Implementation Summary + +1. The Gateway Nodes of each Gateway send requests to the public STUN Server to determine their own NAT type and then synchronize this information to the Gateway CR +2. The relay network is established according to the existing code logic. After the relay network is set up, the relay nodes can obtain the information (ip:port) of the "holes" punched out by each edge node, and this is then synchronized to the Gateway Custom Resource (CR). +3. Each edge node then, based on its own and the remote node's NAT type, determines whether an edge-to-edge tunnel can be created and uses the "hole" punched out on the remote node to establish the actual connection. + +### Implementation Details + +#### Retrieve Hole Punching Information + +wireguard:execute `wg show` on the relay node. + +``` +interface: raven-wg0 + public key: EdZ6TFZMuQ7zk0Cz8U8yKdpotwiiTVcFzYEHGgb8smw= + private key: (hidden) + listening port: 4500 + +peer: aX97MGTV6tzLFl40JXkt6gW1LTaPyb3CpERJTaNKIHU= + preshared key: (hidden) + endpoint: xxx.xxx.xx.xx:21005 + allowed ips: 10.244.13.0/24 + latest handshake: 35 seconds ago + transfer: 180 B received, 612 B sent + persistent keepalive: every 5 seconds + +peer: 9bpg1yii/dxo+0mGHDaEroqpoC0EyN01oD06VypXb28= + preshared key: (hidden) + endpoint: xxx.xxx.xx.xx:65353 + allowed ips: 10.244.1.0/24 + latest handshake: 35 seconds ago + transfer: 180 B received, 644 B sent + persistent keepalive: every 5 seconds + +peer: Wau0C5r6psS3VYjnjVQPMTABi2Ljy3kmMs4DFoCgols= + preshared key: (hidden) + endpoint: xxx.xxx.xx.xx:4500 + allowed ips: 10.244.9.0/24 + latest handshake: 40 seconds ago + transfer: 180 B received, 496 B sent + persistent keepalive: every 5 seconds + +peer: nZNAS5sGBzdAz/PIhfLIxsgYKFblmG5WKAdwxHzL+Dg= + preshared key: (hidden) + endpoint: xxx.xxx.xx.xx:4500 + allowed ips: 10.244.12.0/24 + latest handshake: 40 seconds ago + transfer: 180 B received, 496 B sent + persistent keepalive: every 5 seconds +``` + +libreswan:Retrieve information about the punched holes by parsing the output of `/usr/libexec/ipsec/whack --showstates` + +``` +tencent-centos-node:/# /usr/libexec/ipsec/whack --showstates +000 #25: "10.0.16.6-10.10.104.84-10.244.0.0/24-10.244.9.0/24"[1] 60.191.89.26:4500 STATE_V2_ESTABLISHED_IKE_SA (established IKE SA); REKEY in 28448s; REPLACE in 28718s; newest; idle; +000 #27: "10.0.16.6-10.10.104.84-10.244.0.0/24-10.244.9.0/24"[1] 60.191.89.26:4500 STATE_V2_ESTABLISHED_CHILD_SA (established Child SA); REKEY in 28448s; REPLACE in 28718s; newest; eroute owner; IKE SA #25; idle; +000 #27: "10.0.16.6-10.10.104.84-10.244.0.0/24-10.244.9.0/24"[1] 60.191.89.26 esp.8be5db37@60.191.89.26 esp.d7b50df@10.0.16.6 tun.0@60.191.89.26 tun.0@10.0.16.6 Traffic: ESPin=0B ESPout=0B ESPmax=2^63B +``` + +#### Gateway API + +Add `NATType` and `PublicPort` fields to the Endpoint type, used to save NAT type and hole-punching information. + +```go +// Gateway is the Schema for the gateways API +type Gateway struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GatewaySpec `json:"spec,omitempty"` + Status GatewayStatus `json:"status,omitempty"` +} + +// GatewaySpec defines the desired state of Gateway +type GatewaySpec struct { + // NodeSelector is a label query over nodes that managed by the gateway. + // The nodes in the same gateway should share same layer 3 network. + NodeSelector *metav1.LabelSelector `json:"nodeSelector,omitempty"` + // TODO add a field to configure using vxlan or host-gw for inner gateway communication? + // Endpoints is a list of available Endpoint. + Endpoints []Endpoint `json:"endpoints"` + // ExposeType determines how the Gateway is exposed. + ExposeType ExposeType `json:"exposeType,omitempty"` +} + +// Endpoint stores all essential data for establishing the VPN tunnel. +// TODO add priority field? +type Endpoint struct { + // NodeName is the Node hosting this endpoint. + NodeName string `json:"nodeName"` + UnderNAT bool `json:"underNAT,omitempty"` + NATType string `json:"natType,omitempty"` + PublicIP string `json:"publicIP,omitempty"` + PublicPort string `json:"publicPort,omitempty"` + Config map[string]string `json:"config,omitempty"` +} +``` + +#### Create Edge-To-Edge Tunnel Operation + +For WireGuard, once the edge node has retrieved the relevant information from the Gateway Custom Resource (CR), it can establish an edge-to-edge tunnel by following these steps: + +nodeA: + +``` +wg set raven-wg0 peer allowed-ips endpoint : persistent-keepalive 5 +``` + +nodeB: + +``` +wg set raven-wg0 peer allowed-ips endpoint : persistent-keepalive 5 +``` + +For libreswan, once the edge node has retrieved the relevant information from the Gateway Custom Resource (CR), it can establish an edge-to-edge tunnel by following these steps: + +nodeA: + +``` +/usr/libexec/ipsec/whack --delete --name --- + +/usr/libexec/ipsec/whack --psk --encrypt --forceencaps --name --- --id @-- --host --client --to --id @-- --host --client --ikeport + +/usr/libexec/ipsec/whack --route --name --- + +/usr/libexec/ipsec/whack --initiate --asynchronous --name --- +``` + +nodeB: + +``` +/usr/libexec/ipsec/whack --delete --name --- + +/usr/libexec/ipsec/whack --psk --encrypt --forceencaps --name --- --id @-- --host --client --to --id @-- --host --client --ikeport + +/usr/libexec/ipsec/whack --route --name --- + +/usr/libexec/ipsec/whack --initiate --asynchronous --name --- +``` + +### Hole Punching Effect + +For example, nodeA (located in Hangzhou, Zhejiang) and nodeB (located in Heyuan, Guangdong), with the relay node in Shanghai. + +During relay forwarding: + +``` +[root@fedora-1 /]# ping 10.244.12.4 -c 10 +PING 10.244.12.4 (10.244.12.4) 56(84) bytes of data. +64 bytes from 10.244.12.4: icmp_seq=1 ttl=61 time=54.7 ms +64 bytes from 10.244.12.4: icmp_seq=2 ttl=61 time=54.7 ms +64 bytes from 10.244.12.4: icmp_seq=3 ttl=61 time=54.7 ms +64 bytes from 10.244.12.4: icmp_seq=4 ttl=61 time=54.6 ms +64 bytes from 10.244.12.4: icmp_seq=5 ttl=61 time=54.6 ms +64 bytes from 10.244.12.4: icmp_seq=6 ttl=61 time=54.6 ms +64 bytes from 10.244.12.4: icmp_seq=7 ttl=61 time=54.7 ms +64 bytes from 10.244.12.4: icmp_seq=8 ttl=61 time=54.6 ms +64 bytes from 10.244.12.4: icmp_seq=9 ttl=61 time=55.4 ms +64 bytes from 10.244.12.4: icmp_seq=10 ttl=61 time=54.7 ms + +--- 10.244.12.4 ping statistics --- +10 packets transmitted, 10 received, 0% packet loss, time 9012ms +rtt min/avg/max/mdev = 54.574/54.722/55.427/0.237 ms +``` + +Direct edge-to-edge communication: + +``` +[root@fedora-1 /]# ping 10.244.12.4 -c 10 +PING 10.244.12.4 (10.244.12.4) 56(84) bytes of data. +64 bytes from 10.244.12.4: icmp_seq=1 ttl=62 time=37.4 ms +64 bytes from 10.244.12.4: icmp_seq=2 ttl=62 time=37.4 ms +64 bytes from 10.244.12.4: icmp_seq=3 ttl=62 time=37.4 ms +64 bytes from 10.244.12.4: icmp_seq=4 ttl=62 time=37.4 ms +64 bytes from 10.244.12.4: icmp_seq=5 ttl=62 time=37.3 ms +64 bytes from 10.244.12.4: icmp_seq=6 ttl=62 time=37.4 ms +64 bytes from 10.244.12.4: icmp_seq=7 ttl=62 time=37.4 ms +64 bytes from 10.244.12.4: icmp_seq=8 ttl=62 time=37.3 ms +64 bytes from 10.244.12.4: icmp_seq=9 ttl=62 time=37.5 ms +64 bytes from 10.244.12.4: icmp_seq=10 ttl=62 time=37.3 ms + +--- 10.244.12.4 ping statistics --- +10 packets transmitted, 10 received, 0% packet loss, time 9009ms +rtt min/avg/max/mdev = 37.333/37.372/37.488/0.042 ms +``` \ No newline at end of file From f1262a617f4534468ca7dbb436ae150cb4126578 Mon Sep 17 00:00:00 2001 From: vie-serendipity <60083692+vie-serendipity@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:15:25 +0800 Subject: [PATCH 84/93] Proposal for Multi-region workloads configuration rendering engine (#1600) * proposal about yurtappconfigbinding * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * modify * fix misspell * add some contents * fix unordered list indentation * fix indentation * modify * modify * modify * modify * comparison with OCM * modify * supplement comparison with other projects * update the CRD of yurtappoverrider * lint markdown files * lint markdown files * lint markdown files * lint markdown files * supplement some information * remove upgradeStrategy in Item * alter operator to operation * update Inspiration.png --- docs/img/yurtappoverrider/Architecture.png | Bin 0 -> 178932 bytes docs/img/yurtappoverrider/Inspiration.png | Bin 0 -> 154785 bytes docs/proposals/20230706-yurtappoverrider.md | 332 ++++++++++++++++++++ 3 files changed, 332 insertions(+) create mode 100644 docs/img/yurtappoverrider/Architecture.png create mode 100644 docs/img/yurtappoverrider/Inspiration.png create mode 100644 docs/proposals/20230706-yurtappoverrider.md diff --git a/docs/img/yurtappoverrider/Architecture.png b/docs/img/yurtappoverrider/Architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..963e4dffa0e3195b44db898cea6eea35b484944d GIT binary patch literal 178932 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 z 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 TI@0nf3$S5+RD0M~FhKYCjdhi$X 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%0D3B;fL{ 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 literal 0 HcmV?d00001 diff --git a/docs/proposals/20230706-yurtappoverrider.md b/docs/proposals/20230706-yurtappoverrider.md new file mode 100644 index 00000000000..2ce098a2a5c --- /dev/null +++ b/docs/proposals/20230706-yurtappoverrider.md @@ -0,0 +1,332 @@ +--- +title: Proposal about YurtAppOverrider +authors: + - "@vie-serendipity" + - "@rambohe-ch" +reviewers: + - "" +creation-date: +last-updated: +status: +--- + +# Proposal for Multi-region workloads configuration rendering engine + +* [Proposal for Multi-region workloads configuration rendering engine](#proposal-for-multi-region-workloads-configuration-rendering-engine) + * [Glossary](#glossary) + * [YurtAppOverrider](#yurtappoverrider) + * [Summary](#summary) + * [Motivation](#motivation) + * [Goals](#goals) + * [Non-Goals/Future Work](#non-goalsfuture-work) + * [Proposal](#proposal) + * [Inspiration](#inspiration) + * [YurtAppOverrider API](#yurtappoverrider-api) + * [Architecture](#architecture) + * [Implementation Details](#implementation-details) + * [Deployment Mutating Webhook](#deployment-mutating-webhook) + * [Prerequisites for webhook (Resolving circular dependency)](#prerequisites-for-webhook-resolving-circular-dependency) + * [Workflow of mutating webhook](#workflow-of-mutating-webhook) + * [YurtAppOverrider Validating Webhook](#yurtappoverrider-validating-webhook) + * [YurtAppOverrider Controller](#yurtappoverrider-controller) + * [Task 1](#task-1) + * [Task 2](#task-2) + * [User Stories](#user-stories) + * [Story 1 (General)](#story-1-general) + * [Story 2 (Specific)](#story-2-specific) + * [Story 3 (Gray Release)](#story-3-gray-release) + * [Story 4 (Specify Registry)](#story-4-specify-registry) + * [Story 5 (Customize hostPath)](#story-5-customize-hostpath) + * [Comparison with existing open source projects](#comparison-with-existing-open-source-projects) + * [Open Cluster Management](#open-cluster-management) + * [KubeVela](#kubevela) + * [Implementation History](#implementation-history) + +## Glossary +### YurtAppOverrider +YurtAppOverrider is a new CRD used to customize the configuration of the workloads managed by YurtAppSet/YurtAppDaemon. It provides a simple and straightforward way to configure every field of the workload under each nodepool. It is fundamental component of multi-region workloads configuration rendering engine. +## Summary +Due to the objective existence of heterogeneous environments such as resource configurations and network topologies in each geographic region, the workload configuration is always different in each region. We design a multi-region workloads configuration rendering engine by introducing YurtAppOverrider CRD, relevant controller, and webhooks. The workloads(Deployment/StatefulSet) of nodepools in different regions can be rendered through simple configuration by using YurtAppOverrider which also supports multiple resources(YurtAppSet/YurtAppDaemon). +## Motivation +YurtAppSet is proposed for homogeneous workloads. YurtAppSet is not user-friendly and scalable, although it can be used for workload configuration by patch field. Therefore, we expect a rendering engine to configure workloads in different regions easily, including replicas, images, configmap, secret, pvc, etc. In addition, it is essential to support rendering of existing resources, like YurtAppSet and YurtAppDaemon, and future resources. +### Goals +1. Customize the workloads in different regions +2. Implement GrayRelease through this +3. Specify the registry of the image to adapt the edge network +### Non-Goals/Future Work +1. Optimize YurtAppSet(about status, patch, and replicas) +2. Optimize YurtAppDaemon(about status) +## Proposal +### Inspiration +Reference to the design of ClusterRole and ClusterRoleBinding. + +1. Considering the simplicity of customized rendering configuration, an incremental-like approach is used to implement injection, i.e., only the parts that need to be modified need to be declared, including image and replicas. Therefore, it is reasonable to abstract these configurable fields into an Item. The design of Item refers to the design of VolumeSource in kubernetes. +2. In order to inject item into the workloads, we should create a new CRD named YurtAppOverrider, which consist of items and patches. Items replace a set of configuration for matching nodepools. + +3. Patch supports more advanced add, delete and replace operations, similar to kubectl's json patch. We can convert a patch struct into an API interface call. + + ```shell + kubectl patch deployment xxx --type='json' --patch='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"tomcat"}]' + ``` +### YurtAppOverrider API +1. YurtAppOverrider needs to be bound to YurtAppSet/YurtAppDaemon. +Considering that there are multiple Deployment/StatefulSet per nodepool, as shown below, it must be bound to YurtAppSet/YurtAppDaemon for injection. We use subject field to bind it to YurtAppSet/YurtAppDaemon. +2. YurtAppOverrider is responsible for injection of entries. We only need to create a new YurtAppOverrider resource for all nodepools under a YurtAppSet/YurtAppDaemon resource. + +```go +// ImageItem specifies the corresponding container and the claimed image +type ImageItem struct { + // ContainerName represents name of the container + // in which the Image will be replaced + ContainerName string `json:"containerName"` + // ImageClaim represents the claimed image name + //which is injected into the container above + ImageClaim string `json:"imageClaim"` +} + +// Item represents configuration to be injected. +// Only one of its members may be specified. +type Item struct { + // +optional + Image *ImageItem `json:"image,omitempty"` + // +optional + Replicas *int32 `json:"replicas,omitempty"` +} + +type Operation string + +const ( + ADD Operation = "add" // json patch + REMOVE Operation = "remove" // json patch + REPLACE Operation = "replace" // json patch +) + +type Patch struct { + // Path represents the path in the json patch + Path string `json:"path"` + // type represents the operation + // +kubebuilder:validation:Enum=add;remove;replace + Operation Operation `json:"operation"` + // Indicates the patch for the template + // +optional + Value apiextensionsv1.JSON `json:"value,omitempty"` +} + +// Describe detailed multi-region configuration of the subject +// Entry describe a set of nodepools and their shared or identical configurations +type Entry struct { + Pools []string `json:"pools"` + // +optional + Items []Item `json:"items,omitempty"` + // Convert Patch struct into json patch operation + // +optional + Patches []Patch `json:"patches,omitempty"` +} + +// Describe the object Entries belongs +type Subject struct { + metav1.TypeMeta `json:",inline"` + // Name is the name of YurtAppSet or YurtAppDaemon + Name string `json:"name"` +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=yacr +// +kubebuilder:printcolumn:name="Subject_Kind",type="string",JSONPath=".subject.kind",description="The subject kind of this overrider." +// +kubebuilder:printcolumn:name="Subject_Name",type="string",JSONPath=".subject.Name",description="The subject name of this overrider." +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp",description="CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC." + +type YurtAppOverrider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Subject Subject `json:"subject"` + Entries []Entry `json:"entries"` +} +``` +### Architecture +The whole architecture is shown below. + +### Implementation Details +#### Deployment Mutating Webhook +##### Prerequisites for webhook (Resolving circular dependency) +Since YurtManager is deployed as a Deployment, the Deployment webhook and YurtManager create a circular dependency. + +Solutions: +1. Change YurtManager deploying method, like static pod +2. Controller is responsible for both creating and updating. However, there will be a period of unavailability(wrong configuration information) +3. Webhook's failurePolicy set to ignore(difficult to detect in the case of malfunction) +4. YurtManager is in charge of managing the webohok, we can modify the internal implementation of YurtManager(Recommended) +##### Workflow of mutating webhook +1. If the intercepted Deployment's ownerReferences field is empty, filter it directly +2. Find the corresponding YurtAppOverrider resource by ownerReferences, if not, filter directly +3. Find the entries involved, get the corresponding configuration, and inject them into workloads. + +Attention Points: +1. Note that injection is implemented by recalculating the final configuration according to the YurtAppSet workload template and the watching YurtAppOverrider +2. The latter configuration always relpace the former. So the last configuration will really work +#### YurtAppOverrider Validating Webhook +1. Verify that only one YurtAppOverrider can be bound to YurtAppSet/YurtAppDaemon +2. Verify that value is empty when operation is REMOVE +#### YurtAppOverrider Controller +##### Task 1 +1. Get update events by watching the YurtAppOverrider resource +2. Trigger the Deployment mutating webhook by modifying an annotation or label +##### Task 2 +1. Get delete events(delete members of pools) by watching the YurtAppOverrider resource +2. Render the configuration according to the YurtAppSet workload template and the watching YurtAppOverrider +### User Stories +#### Story 1 (General) +Use YurtAppSet with YurtAppOverrider for customized configuration of each region. Create YurtAppOverrider first and then create YurtAppSet. If update is needed, modify YurtAppSet resource directly. For YurtAppDaemon, the usage is similar. Users only need to do some configurations in YurtAppOverrider and our rendering engine will inject all configurations into target workloads. +#### Story 2 (Specific) +For example, if there are three locations, Beijing and Hangzhou have the similar configuration, and Shanghai is not the same. They have different image version, replicas. We can configure it as follows: +```yaml +apiVersion: apps.openyurt.io/v1alpha1 +kind: YurtAppOverrider +metadata: + namespace: default + name: demo1 +subject: + apiVersion: apps.openyurt.io/v1alpha1 + kind: YurtAppSet + nameSpace: default + name: yurtappset-demo +entries: +- pools: + beijing + hangzhou + items: + - image: + containerName: nginx + imageClaim: nginx:1.14.2 + - replicas: 3 +- pools: + shanghai + items: + - image: + containerName: nginx + imageClaim: nginx:1.13.2 + - replicas: 5 +``` +#### Story 3 (Gray Release) +Do Gray Release in hangzhou. +```yaml +apiVersion: apps.openyurt.io/v1alpha1 +kind: YurtAppOverrider +metadata: + namespace: default + name: demo1 +subject: + apiVersion: apps.openyurt.io/v1alpha1 + kind: YurtAppSet + nameSpace: default + name: yurtappset-demo +entries: +- pools: + hangzhou + items: + - image: + containerName: demo + imageClaim: xxx:latest +``` +#### Story 4 (Specify Registry) +Specify detailed registry to solve the problem of edge network unreachability. +```yaml +apiVersion: apps.openyurt.io/v1alpha1 +kind: YurtAppOverrider +metadata: + namespace: default + name: demo1 +subject: + apiVersion: apps.openyurt.io/v1alpha1 + kind: YurtAppSet + nameSpace: default + name: yurtappset-demo +entries: +- pools: + hangzhou + items: + - image: + containerName: demo + imageClaim: :/: +``` +#### Story 5 (Customize hostPath) +Use different hostPath in different regions. +```yaml +apiVersion: apps.openyurt.io/v1alpha1 +kind: YurtAppOverrider +metadata: + namespace: default + name: demo1 +subject: + apiVersion: apps.openyurt.io/v1alpha1 + kind: YurtAppSet + nameSpace: default + name: yurtappset-demo +entries: +- pools: + beijing + items: + - image: + containerName: nginx + imageClaim: nginx:1.14.2 +- pools: + hangzhou + patches: + - operation: add + path: /spec/template/spec/volumes/- + value: + name: test-volume + hostPath: + path: /var/lib/docker + type: Directory + - operation: replace + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: shared-dir + mountPath: /var/lib/docker +- pools: + beijing + patches: + - operation: add + path: /spec/template/spec/volumes/- + value: + name: test-volume + hostPath: + path: /data/logs + type: Directory + - operation: replace + path: /spec/template/spec/containers/0/volumeMounts/- + value: + name: shared-dir + mountPath: /data/logs +``` +### Comparison with existing open source projects +#### Open Cluster Management +Multicluster and multicloud management systems, such as Open Cluster Management(OCM), mainly focus unified management of multiple clusters. It provides ManifestWork and Placement. +ManifestWork provides the ability to send workloads down to the target cluster. +Placement is used to dynamically select a set of managedClusters in one or multiple ManagedClusterSet so that higher level users can either replicate Kubernetes resources to the member clusters or run their advanced workload i.e. multi-cluster scheduling. + +Advantages: +1. OCM uses ManifestWorkReplicaSet(aggregator of Manifestwork and Placement) for this, focusing more on schedule's strategy, predicates and priority. +2. OCM provides much information on status and supports fine-grained field values tracking. + +Disadvantages: +1. For workloads with different configurations, it requires multiple Manifestwork to deploy. +2. It does not work with current components, including yurtapp, yurtappdaemon. +#### KubeVela +KubeVela is a modern software delivery platform that makes deploying and operating applications across today's hybrid, multi-cloud environments easier, faster and more reliable. + +Advantages: +- KubeVela can achieve the distribution and deployment of workloads, +Utilizing component replication function and json-patch trait, users and operators can realize the customized configuration of nodepools. + +Disadvantages: +- It cannot accomplish dynamic deployment for each new nodepool, while yurtappdaemon can make it. +## Implementation History +- [ ] YurtAppOverrider API CRD +- [ ] Deployment Mutating Webhook +- [ ] YurtAppOverrider controller +- [ ] Resolve circular dependency +- [ ] YurtAppOverrider validating webhook \ No newline at end of file From 13762e95d2a52caa6c0f5b605f9ddaac34653a84 Mon Sep 17 00:00:00 2001 From: Nico Wang <104056727+wangzihao05@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:35:24 +0800 Subject: [PATCH 85/93] fix syntax errors (#1669) --- docs/proposals/00_openyurt-glossary.md | 2 +- pkg/yurttunnel/kubernetes/kubernetes.go | 2 +- pkg/yurttunnel/server/anpserver.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/proposals/00_openyurt-glossary.md b/docs/proposals/00_openyurt-glossary.md index 340e3035567..e833b6c36f7 100644 --- a/docs/proposals/00_openyurt-glossary.md +++ b/docs/proposals/00_openyurt-glossary.md @@ -45,7 +45,7 @@ Controller Manager for OpenYurt like Kube Controller Manager for K8s, and includ ### YurtHub -A reverse proxy and cache response on local disk. when cloud-edge network is normal, forward the requests from edge to cloud, and when cloud-edge network is disconnected, the local cache is returned to edge client. +A reverse proxy and cache response on local disk. When cloud-edge network is normal, forward the requests from edge to cloud, and when cloud-edge network is disconnected, the local cache is returned to edge client. ### YurtTunnel diff --git a/pkg/yurttunnel/kubernetes/kubernetes.go b/pkg/yurttunnel/kubernetes/kubernetes.go index 020ae1db970..0e63b7285cb 100644 --- a/pkg/yurttunnel/kubernetes/kubernetes.go +++ b/pkg/yurttunnel/kubernetes/kubernetes.go @@ -31,7 +31,7 @@ import ( ) // CreateClientSet creates a clientset based on the given kubeConfig. If the -// kubeConfig is empty, it will creates the clientset based on the in-cluster +// kubeConfig is empty, it will create the clientset based on the in-cluster // config func CreateClientSet(kubeConfig string) (*kubernetes.Clientset, error) { diff --git a/pkg/yurttunnel/server/anpserver.go b/pkg/yurttunnel/server/anpserver.go index c9d4c36b44f..2c5bc7c69d1 100644 --- a/pkg/yurttunnel/server/anpserver.go +++ b/pkg/yurttunnel/server/anpserver.go @@ -110,7 +110,7 @@ func runProxier(handler http.Handler, return errors.New("DOESN'T SUPPROT EGRESS SELECTOR YET") } // request will be sent from request interceptor on the same host, - // so we use UDS protocol to avoide sending request through kernel + // so we use UDS protocol to avoid sending request through kernel // network stack. go func() { server := &http.Server{ From 47f14dfd6a0955060b6aa862b72389b0b541b18a Mon Sep 17 00:00:00 2001 From: 401lrx <59675900+401lrx@users.noreply.github.com> Date: Thu, 24 Aug 2023 15:38:38 +0800 Subject: [PATCH 86/93] add proposal of install openyurt components using dashboard (#1664) * add proposal of install openyurt components using dashboard --- docs/img/dashboard-config-modal.png | Bin 0 -> 66972 bytes docs/img/dashboard-install-modal.png | Bin 0 -> 31635 bytes docs/img/dashboard-login-page.png | Bin 0 -> 67340 bytes docs/img/dashboard-manage-page.png | Bin 0 -> 157181 bytes ...all-openyurt-components-using-dashboard.md | 118 ++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 docs/img/dashboard-config-modal.png create mode 100644 docs/img/dashboard-install-modal.png create mode 100644 docs/img/dashboard-login-page.png create mode 100644 docs/img/dashboard-manage-page.png create mode 100644 docs/proposals/20230820-install-openyurt-components-using-dashboard.md diff --git a/docs/img/dashboard-config-modal.png b/docs/img/dashboard-config-modal.png new file mode 100644 index 0000000000000000000000000000000000000000..340cd2b4fb31fc735091d4ca108f32e4630824f4 GIT binary patch literal 66972 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

    D)6!Ac5l}93a7h@q46cRtAc(_xYk+Wr)e2c%q{# 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 Y>P-_DyTsJM-vv&dIDI_t*!kQ41)*r6CIA2c literal 0 HcmV?d00001 diff --git a/docs/img/dashboard-login-page.png b/docs/img/dashboard-login-page.png new file mode 100644 index 0000000000000000000000000000000000000000..c43294ca21dc3c48bf7460b205941afbcdcd91d7 GIT binary patch literal 67340 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∋*1D7x-;Q?kh-46R~F=03G<0 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 MRFo)rY2f>R0N<|ZIsgCw literal 0 HcmV?d00001 diff --git a/docs/img/dashboard-manage-page.png b/docs/img/dashboard-manage-page.png new file mode 100644 index 0000000000000000000000000000000000000000..0b5eafac88041fe076fb18704ef44b7ea93d562e GIT binary patch literal 157181 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 z zSCJLWlH2TurJK}3aGH~;sej{3dcfK0a1Z!WL-gZENcrWIn$a;Br4}5NQ4<$A1T&|H zkg)A1J9n2+dh4&)4?u>Ou9%HF61;247H(R2U_apY9*c18hx^K318uD{C-OQonO-wFJ5^bJ3y)i0`9-cO2Ilkf}S z3fjq^fx_6bO)s4B*2VAm;kK_=W`uV0o>cWm)1V6?X372@buv^ z!NMZ5GP2)QST3Xo{mdR{tswQiX45Z29g1FxC8bo>Z + + + +##### Installation Page + +Version selection and configuration file editing are provided for a more flexible installation process. + +

    + +
    +
    + +
    + +##### The first login screen for users + +When visit the page for the first time after installing the dashboard, a guide page will be displayed. Check the components and click Next to complete the installation of the component. + +
    + +
    From 68d4079f697d391e929c42ef0324fef731bb785f Mon Sep 17 00:00:00 2001 From: wesleysu <59680532+River-sh@users.noreply.github.com> Date: Thu, 24 Aug 2023 15:38:53 +0800 Subject: [PATCH 87/93] add raven l7 dns controller (#1658) Co-authored-by: Wesley Su --- cmd/yurt-manager/app/options/options.go | 4 + .../raven/well_known_labels_annotations.go | 3 +- pkg/yurtmanager/controller/controller.go | 2 + pkg/yurtmanager/controller/raven/common.go | 1 + .../controller/raven/dns/dns_controller.go | 261 ++++++++++++++++++ .../raven/dns/dns_controller_test.go | 167 +++++++++++ .../raven/dns/dns_enqueue_handlers.go | 105 +++++++ .../raven/dns/dns_enqueue_handlers_test.go | 118 ++++++++ .../controller/raven/utils/utils.go | 14 +- 9 files changed, 672 insertions(+), 3 deletions(-) create mode 100644 pkg/yurtmanager/controller/raven/dns/dns_controller.go create mode 100644 pkg/yurtmanager/controller/raven/dns/dns_controller_test.go create mode 100644 pkg/yurtmanager/controller/raven/dns/dns_enqueue_handlers.go create mode 100644 pkg/yurtmanager/controller/raven/dns/dns_enqueue_handlers_test.go diff --git a/cmd/yurt-manager/app/options/options.go b/cmd/yurt-manager/app/options/options.go index 369c5ff944f..1d1237b46b1 100644 --- a/cmd/yurt-manager/app/options/options.go +++ b/cmd/yurt-manager/app/options/options.go @@ -92,6 +92,10 @@ func (y *YurtManagerOptions) ApplyTo(c *config.Config) error { if err := y.PlatformAdminController.ApplyTo(&c.ComponentConfig.PlatformAdminController); err != nil { return err } + if err := y.GatewayPickupController.ApplyTo(&c.ComponentConfig.GatewayPickupController); err != nil { + return err + } + return nil } diff --git a/pkg/apis/raven/well_known_labels_annotations.go b/pkg/apis/raven/well_known_labels_annotations.go index bf52194bc82..7a1ed4ae707 100644 --- a/pkg/apis/raven/well_known_labels_annotations.go +++ b/pkg/apis/raven/well_known_labels_annotations.go @@ -18,5 +18,6 @@ package raven const ( // LabelCurrentGateway indicates which gateway the node is currently belonging to - LabelCurrentGateway = "raven.openyurt.io/gateway" + LabelCurrentGateway = "raven.openyurt.io/gateway" + LabelCurrentGatewayType = "raven.openyurt.io/gateway-type" ) diff --git a/pkg/yurtmanager/controller/controller.go b/pkg/yurtmanager/controller/controller.go index 2df60ea13c1..297c990dd99 100644 --- a/pkg/yurtmanager/controller/controller.go +++ b/pkg/yurtmanager/controller/controller.go @@ -27,6 +27,7 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/dns" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" servicetopologyendpoints "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/endpoints" @@ -56,6 +57,7 @@ func init() { controllerAddFuncs[delegatelease.ControllerName] = []AddControllerFn{delegatelease.Add} controllerAddFuncs[podbinding.ControllerName] = []AddControllerFn{podbinding.Add} controllerAddFuncs[raven.GatewayPickupControllerName] = []AddControllerFn{gatewaypickup.Add} + controllerAddFuncs[raven.GatewayDNSControllerName] = []AddControllerFn{dns.Add} controllerAddFuncs[nodepool.ControllerName] = []AddControllerFn{nodepool.Add} controllerAddFuncs[yurtcoordinatorcert.ControllerName] = []AddControllerFn{yurtcoordinatorcert.Add} controllerAddFuncs[servicetopology.ControllerName] = []AddControllerFn{servicetopologyendpoints.Add, servicetopologyendpointslice.Add} diff --git a/pkg/yurtmanager/controller/raven/common.go b/pkg/yurtmanager/controller/raven/common.go index 3ea1fd29fcd..583c3be51e0 100644 --- a/pkg/yurtmanager/controller/raven/common.go +++ b/pkg/yurtmanager/controller/raven/common.go @@ -23,4 +23,5 @@ var ( const ( ControllerName = "gateway" GatewayPickupControllerName = "raven-gateway-pickup" + GatewayDNSControllerName = "raven-dns" ) diff --git a/pkg/yurtmanager/controller/raven/dns/dns_controller.go b/pkg/yurtmanager/controller/raven/dns/dns_controller.go new file mode 100644 index 00000000000..43a3e4435c7 --- /dev/null +++ b/pkg/yurtmanager/controller/raven/dns/dns_controller.go @@ -0,0 +1,261 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dns + +import ( + "context" + "fmt" + "net" + "sort" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +func Format(format string, args ...interface{}) string { + s := fmt.Sprintf(format, args...) + return fmt.Sprintf("%s: %s", common.GatewayDNSControllerName, s) +} + +// Add creates a new Ravendns Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +var _ reconcile.Reconciler = &ReconcileDns{} + +type ReconcileDns struct { + client.Client + scheme *runtime.Scheme + recorder record.EventRecorder +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileDns{ + Client: mgr.GetClient(), + scheme: mgr.GetScheme(), + recorder: mgr.GetEventRecorderFor(common.GatewayDNSControllerName), + } +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New(common.GatewayDNSControllerName, mgr, controller.Options{ + Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, + }) + if err != nil { + return err + } + + // Watch for changes to service + err = c.Watch(&source.Kind{Type: &corev1.Service{}}, &EnqueueRequestForServiceEvent{}, predicate.NewPredicateFuncs( + func(obj client.Object) bool { + svc, ok := obj.(*corev1.Service) + if !ok { + return false + } + if svc.Spec.Type != corev1.ServiceTypeClusterIP { + return false + } + return svc.Namespace == utils.WorkingNamespace && svc.Name == utils.GatewayProxyInternalService + })) + if err != nil { + return err + } + //Watch for changes to nodes + err = c.Watch(&source.Kind{Type: &corev1.Node{}}, &EnqueueRequestForNodeEvent{}) + if err != nil { + return err + } + + return nil +} + +func (r *ReconcileDns) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + klog.V(4).Info(Format("Reconcile DNS configMap for gateway %s", req.Name)) + defer func() { + klog.V(4).Info(Format("finished DNS configMap for gateway %s", req.Name)) + }() + var proxyAddress = "" + //1. ensure configmap to record dns + cm, err := r.getProxyDNS(ctx, client.ObjectKey{Namespace: utils.WorkingNamespace, Name: utils.RavenProxyNodesConfig}) + if err != nil { + return reconcile.Result{Requeue: true, RequeueAfter: 2 * time.Second}, err + } + + // 2. acquired raven global config to check whether the proxy s enabled + enableProxy, _ := utils.CheckServer(ctx, r.Client) + if !enableProxy { + r.recorder.Event(cm.DeepCopy(), corev1.EventTypeNormal, "MaintainDNSRecord", "The Raven Layer 7 proxy feature is not enabled for the cluster") + } else { + svc, err := r.getService(ctx, types.NamespacedName{Namespace: utils.WorkingNamespace, Name: utils.GatewayProxyInternalService}) + if err != nil && !apierrors.IsNotFound(err) { + klog.V(2).Infof(Format("failed to get service %s/%s", utils.WorkingNamespace, utils.GatewayProxyInternalService)) + return reconcile.Result{Requeue: true, RequeueAfter: 2 * time.Second}, err + } + if apierrors.IsNotFound(err) || svc.DeletionTimestamp != nil { + r.recorder.Event(cm.DeepCopy(), corev1.EventTypeNormal, "MaintainDNSRecord", + fmt.Sprintf("The Raven Layer 7 proxy lacks service %s/%s", utils.WorkingNamespace, utils.GatewayProxyInternalService)) + } + if svc != nil { + if svc.Spec.ClusterIP == "" { + r.recorder.Event(cm.DeepCopy(), corev1.EventTypeNormal, "MaintainDNSRecord", + fmt.Sprintf("The service %s/%s cluster IP is empty", utils.WorkingNamespace, utils.GatewayProxyInternalService)) + } else { + proxyAddress = svc.Spec.ClusterIP + } + } + } + + //3. update dns record + nodeList := new(corev1.NodeList) + err = r.Client.List(ctx, nodeList, &client.ListOptions{}) + if err != nil { + return reconcile.Result{Requeue: true, RequeueAfter: 2 * time.Second}, fmt.Errorf("failed to list node, error %s", err.Error()) + } + cm.Data[utils.ProxyNodesKey] = buildDNSRecords(nodeList, enableProxy, proxyAddress) + err = r.updateDNS(cm) + if err != nil { + return reconcile.Result{Requeue: true, RequeueAfter: 2 * time.Second}, fmt.Errorf("failed to update configmap %s/%s, error %s", + cm.GetNamespace(), cm.GetName(), err.Error()) + } + return reconcile.Result{}, nil +} + +func (r ReconcileDns) getProxyDNS(ctx context.Context, objKey client.ObjectKey) (*corev1.ConfigMap, error) { + var cm corev1.ConfigMap + err := wait.PollImmediate(5*time.Second, time.Minute, func() (done bool, err error) { + err = r.Client.Get(ctx, objKey, &cm) + if err != nil { + if apierrors.IsNotFound(err) { + err = r.buildRavenDNSConfigMap() + if err != nil { + klog.Errorf(Format("failed to generate dns record , error %s", err.Error())) + return false, nil + } + } + klog.Error(Format("failed to get ConfigMap %s, error %s", objKey.String(), err.Error())) + return false, nil + } + return true, nil + }) + if err != nil { + return cm.DeepCopy(), fmt.Errorf("failed to get ConfigMap %s, error %s", objKey.String(), err.Error()) + } + return cm.DeepCopy(), nil +} + +func (r *ReconcileDns) buildRavenDNSConfigMap() error { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.RavenProxyNodesConfig, + Namespace: utils.WorkingNamespace, + }, + Data: map[string]string{ + utils.ProxyNodesKey: "", + }, + } + err := r.Client.Create(context.TODO(), cm, &client.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create ConfigMap %s/%s, error %s", cm.GetNamespace(), cm.GetName(), err.Error()) + } + return nil +} + +func (r *ReconcileDns) getService(ctx context.Context, objectKey client.ObjectKey) (*corev1.Service, error) { + svc := corev1.Service{} + err := r.Client.Get(ctx, objectKey, &svc) + if err != nil { + return nil, err + } + return svc.DeepCopy(), nil +} + +func (r *ReconcileDns) updateDNS(cm *corev1.ConfigMap) error { + err := r.Client.Update(context.TODO(), cm, &client.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update configmap %s/%s, %s", cm.GetNamespace(), cm.GetName(), err.Error()) + } + return nil +} + +func buildDNSRecords(nodeList *corev1.NodeList, needProxy bool, proxyIp string) string { + // record node name <-> ip address + if needProxy && proxyIp == "" { + klog.Errorf(Format("internal proxy address is empty for dns record, redirect node internal address")) + needProxy = false + } + var err error + dns := make([]string, 0, len(nodeList.Items)) + for _, node := range nodeList.Items { + ip := proxyIp + if !needProxy { + ip, err = getHostIP(&node) + if err != nil { + klog.Errorf(Format("failed to parse node address for %s, %s", node.Name, err.Error())) + continue + } + } + dns = append(dns, fmt.Sprintf("%s\t%s", ip, node.Name)) + } + sort.Strings(dns) + return strings.Join(dns, "\n") +} + +func getHostIP(node *corev1.Node) (string, error) { + // get InternalIPs first and then ExternalIPs + var internalIP, externalIP net.IP + for _, addr := range node.Status.Addresses { + switch addr.Type { + case corev1.NodeInternalIP: + ip := net.ParseIP(addr.Address) + if ip != nil { + return ip.String(), nil + } + case corev1.NodeExternalIP: + ip := net.ParseIP(addr.Address) + if ip != nil { + externalIP = ip + } + } + } + if internalIP == nil && externalIP == nil { + return "", fmt.Errorf("host IP unknown; known addresses: %v", node.Status.Addresses) + } + return externalIP.String(), nil +} diff --git a/pkg/yurtmanager/controller/raven/dns/dns_controller_test.go b/pkg/yurtmanager/controller/raven/dns/dns_controller_test.go new file mode 100644 index 00000000000..034673839df --- /dev/null +++ b/pkg/yurtmanager/controller/raven/dns/dns_controller_test.go @@ -0,0 +1,167 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dns + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/openyurtio/openyurt/pkg/apis/raven" + ravenv1v1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +const ( + ProxyIP = "172.168.0.1" + Node1Name = "node-1" + Node2Name = "node-2" + Node3Name = "node-3" + Node4Name = "node-4" + Node1Address = "192.168.0.1" + Node2Address = "192.168.0.2" + Node3Address = "192.168.0.3" + Node4Address = "192.168.0.4" + MockGateway = "gw-mock" + MockProxySvc = "x-raven-proxy-internal-svc" +) + +func mockKubeClient() client.Client { + nodeList := &v1.NodeList{ + Items: []v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node1Name, + Labels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + }, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: Node1Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node2Name, + Labels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + }, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: Node2Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node3Name, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: Node3Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node4Name, + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: Node4Address, + }, + }, + }, + }, + }, + } + + services := &v1.ServiceList{ + Items: []v1.Service{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: MockProxySvc, + Namespace: utils.WorkingNamespace, + Labels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + raven.LabelCurrentGatewayType: ravenv1v1beta1.Proxy, + }, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + ClusterIP: ProxyIP, + Ports: []v1.ServicePort{}, + }, + }, + }, + } + + configmaps := &v1.ConfigMapList{ + Items: []v1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: utils.RavenProxyNodesConfig, + Namespace: utils.WorkingNamespace, + }, + Data: map[string]string{ + utils.ProxyNodesKey: "", + }, + }, + }, + } + objs := []runtime.Object{nodeList, configmaps, services} + return fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() +} + +func mockReconciler() *ReconcileDns { + return &ReconcileDns{ + Client: mockKubeClient(), + recorder: record.NewFakeRecorder(100), + } +} + +func TestReconcileDns_Reconcile(t *testing.T) { + r := mockReconciler() + t.Run("get dns configmap", func(t *testing.T) { + res, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Namespace: utils.WorkingNamespace, Name: utils.RavenProxyNodesConfig}}) + assert.Equal(t, reconcile.Result{}, res) + assert.Equal(t, err, nil) + }) +} diff --git a/pkg/yurtmanager/controller/raven/dns/dns_enqueue_handlers.go b/pkg/yurtmanager/controller/raven/dns/dns_enqueue_handlers.go new file mode 100644 index 00000000000..08d4871bb13 --- /dev/null +++ b/pkg/yurtmanager/controller/raven/dns/dns_enqueue_handlers.go @@ -0,0 +1,105 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dns + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/event" + + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +type EnqueueRequestForServiceEvent struct{} + +func (h *EnqueueRequestForServiceEvent) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { + svc, ok := e.Object.(*corev1.Service) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1.Service")) + return + } + if svc.Spec.ClusterIP == "" { + klog.Error(Format("failed to get cluster IP %s/%s", svc.Namespace, svc.Name)) + return + } + + klog.V(2).Infof(Format("enqueue configmap %s/%s due to service create event", utils.WorkingNamespace, utils.RavenProxyNodesConfig)) + utils.AddDNSConfigmapToWorkQueue(q) +} + +func (h *EnqueueRequestForServiceEvent) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { + newSvc, ok := e.ObjectNew.(*corev1.Service) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1.Service")) + return + } + oldSvc, ok := e.ObjectOld.(*corev1.Service) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1.Service")) + return + } + if newSvc.Spec.ClusterIP != oldSvc.Spec.ClusterIP { + klog.V(2).Infof(Format("enqueue configmap %s/%s due to service update event", utils.WorkingNamespace, utils.RavenProxyNodesConfig)) + utils.AddDNSConfigmapToWorkQueue(q) + } +} + +func (h *EnqueueRequestForServiceEvent) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { + _, ok := e.Object.(*corev1.Service) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1.Service")) + return + } + klog.V(2).Infof(Format("enqueue configmap %s/%s due to service update event", utils.WorkingNamespace, utils.RavenProxyNodesConfig)) + utils.AddDNSConfigmapToWorkQueue(q) + return +} + +func (h *EnqueueRequestForServiceEvent) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { + return +} + +type EnqueueRequestForNodeEvent struct{} + +func (h *EnqueueRequestForNodeEvent) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { + _, ok := e.Object.(*corev1.Node) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1.Node")) + return + } + klog.V(2).Infof(Format("enqueue configmap %s/%s due to node create event", utils.WorkingNamespace, utils.RavenProxyNodesConfig)) + utils.AddDNSConfigmapToWorkQueue(q) +} + +func (h *EnqueueRequestForNodeEvent) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { + return +} + +func (h *EnqueueRequestForNodeEvent) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { + _, ok := e.Object.(*corev1.Node) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1.Node")) + return + } + klog.V(2).Infof(Format("enqueue configmap %s/%s due to node delete event", utils.WorkingNamespace, utils.RavenProxyNodesConfig)) + utils.AddDNSConfigmapToWorkQueue(q) +} + +func (h *EnqueueRequestForNodeEvent) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { + +} diff --git a/pkg/yurtmanager/controller/raven/dns/dns_enqueue_handlers_test.go b/pkg/yurtmanager/controller/raven/dns/dns_enqueue_handlers_test.go new file mode 100644 index 00000000000..c91cfe382bf --- /dev/null +++ b/pkg/yurtmanager/controller/raven/dns/dns_enqueue_handlers_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dns + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +func mockService() *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: utils.WorkingNamespace, + Name: utils.GatewayProxyInternalService, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: ProxyIP, + }, + } +} + +func mockNode() *corev1.Node { + return &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: Node1Name, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node1Address, + }, + }, + }, + } +} + +func TestEnqueueRequestFoServiceEvent(t *testing.T) { + h := &EnqueueRequestForServiceEvent{} + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + svc := mockService() + clearQueue := func(queue workqueue.RateLimitingInterface) { + for queue.Len() > 0 { + item, _ := queue.Get() + queue.Done(item) + } + } + h.Create(event.CreateEvent{Object: svc}, queue) + if !assert.Equal(t, 1, queue.Len()) { + t.Errorf("failed to update service, expected %d, but get %d", 1, queue.Len()) + } + clearQueue(queue) + + deletedSvc := svc.DeepCopy() + time := metav1.Now() + deletedSvc.DeletionTimestamp = &time + h.Delete(event.DeleteEvent{Object: deletedSvc}, queue) + if !assert.Equal(t, 1, queue.Len()) { + t.Errorf("failed to update service, expected %d, but get %d", 1, queue.Len()) + } + clearQueue(queue) + + newSvc := svc.DeepCopy() + newSvc.Spec.ClusterIP = "0.0.0.0" + h.Update(event.UpdateEvent{ObjectOld: svc, ObjectNew: newSvc}, queue) + if !assert.Equal(t, 1, queue.Len()) { + t.Errorf("failed to update service, expected %d, but get %d", 1, queue.Len()) + } + clearQueue(queue) +} + +func TestEnqueueRequestForNodeEvent(t *testing.T) { + h := &EnqueueRequestForNodeEvent{} + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + node := mockNode() + clearQueue := func(queue workqueue.RateLimitingInterface) { + for queue.Len() > 0 { + item, _ := queue.Get() + queue.Done(item) + } + } + h.Create(event.CreateEvent{Object: node}, queue) + if !assert.Equal(t, 1, queue.Len()) { + t.Errorf("failed to create node, expected %d, but get %d", 1, queue.Len()) + } + clearQueue(queue) + + time := metav1.Now() + deletedNode := node.DeepCopy() + deletedNode.DeletionTimestamp = &time + h.Delete(event.DeleteEvent{Object: deletedNode}, queue) + if !assert.Equal(t, 1, queue.Len()) { + t.Errorf("failed to create node, expected %d, but get %d", 1, queue.Len()) + } + clearQueue(queue) +} diff --git a/pkg/yurtmanager/controller/raven/utils/utils.go b/pkg/yurtmanager/controller/raven/utils/utils.go index 0bb880c42f0..7219ebde5d8 100644 --- a/pkg/yurtmanager/controller/raven/utils/utils.go +++ b/pkg/yurtmanager/controller/raven/utils/utils.go @@ -31,8 +31,13 @@ import ( ) const ( - WorkingNamespace = "kube-system" - RavenGlobalConfig = "raven-cfg" + WorkingNamespace = "kube-system" + RavenGlobalConfig = "raven-cfg" + GatewayProxyInternalService = "x-raven-proxy-internal-svc" + GatewayProxyServiceNamePrefix = "x-raven-proxy-svc-" + GatewayTunnelServiceNamePrefix = "x-raven-tunnel-svc-" + RavenProxyNodesConfig = "edge-tunnel-nodes" + ProxyNodesKey = "tunnel-nodes" RavenEnableProxy = "EnableL7Proxy" RavenEnableTunnel = "EnableL3Tunnel" @@ -79,5 +84,10 @@ func CheckServer(ctx context.Context, client client.Client) (enableProxy, enable enableTunnel = true } return enableProxy, enableTunnel +} +func AddDNSConfigmapToWorkQueue(q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{Namespace: WorkingNamespace, Name: RavenProxyNodesConfig}, + }) } From a7d29d672f63735915af9a4d0d5446d121d74a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=91=B8=E9=B1=BC=E5=96=B5?= Date: Wed, 30 Aug 2023 17:39:35 +0800 Subject: [PATCH 88/93] unserve v1alpha1 version of platformadmin crd (#1659) Signed-off-by: LavenderQAQ --- charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml | 2 +- pkg/apis/iot/v1alpha1/platformadmin_types.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml b/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml index edb4df7fd76..0b980281c1b 100644 --- a/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml +++ b/charts/yurt-manager/crds/iot.openyurt.io_platformadmins.yaml @@ -8380,7 +8380,7 @@ spec: type: integer type: object type: object - served: true + served: false storage: false subresources: status: {} diff --git a/pkg/apis/iot/v1alpha1/platformadmin_types.go b/pkg/apis/iot/v1alpha1/platformadmin_types.go index b32d110beff..d108599164a 100644 --- a/pkg/apis/iot/v1alpha1/platformadmin_types.go +++ b/pkg/apis/iot/v1alpha1/platformadmin_types.go @@ -110,6 +110,7 @@ type PlatformAdminCondition struct { // +kubebuilder:printcolumn:name="Deployment",type="integer",JSONPath=".status.deploymentReplicas",description="The Deployment Replica." // +kubebuilder:printcolumn:name="ReadyDeployment",type="integer",JSONPath=".status.deploymentReadyReplicas",description="The Ready Deployment Replica." // +kubebuilder:deprecatedversion:warning="iot.openyurt.io/v1alpha1 PlatformAdmin will be deprecated in future; use iot.openyurt.io/v1alpha2 PlatformAdmin; v1alpha1 PlatformAdmin.Spec.ServiceType only support ClusterIP" +// +kubebuilder:unservedversion // PlatformAdmin is the Schema for the samples API type PlatformAdmin struct { From 0dbc3f161ee300634a23e68e6de1b2304cf5eb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=91=B8=E9=B1=BC=E5=96=B5?= Date: Wed, 30 Aug 2023 17:57:35 +0800 Subject: [PATCH 89/93] fix: yurt-iot-dock cannot be dynamically deployed in platformadmin (#1679) Signed-off-by: LavenderQAQ --- .../platformadmin/platformadmin_controller.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go index 46686d11dab..6d01a0ea155 100644 --- a/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go +++ b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go @@ -777,12 +777,6 @@ func (r *ReconcilePlatformAdmin) initFramework(ctx context.Context, platformAdmi r.calculateDesiredComponents(platformAdmin, platformAdminFramework, nil) } - yurtIotDock, err := newYurtIoTDockComponent(platformAdmin, platformAdminFramework) - if err != nil { - return err - } - platformAdminFramework.Components = append(platformAdminFramework.Components, yurtIotDock) - // For better serialization, the serialization method of the Kubernetes runtime library is used data, err := runtime.Encode(r.yamlSerializer, platformAdminFramework) if err != nil { @@ -862,6 +856,16 @@ func (r *ReconcilePlatformAdmin) calculateDesiredComponents(platformAdmin *iotv1 } } + // The yurt-iot-dock is maintained by openyurt and is not obtained through an auto-collector. + // Therefore, it needs to be handled separately + if addedComponentSet.Has(util.IotDockName) { + yurtIotDock, err := newYurtIoTDockComponent(platformAdmin, platformAdminFramework) + if err != nil { + klog.Errorf(Format("newYurtIoTDockComponent error %v", err)) + } + desiredComponents = append(desiredComponents, yurtIotDock) + } + // TODO: In order to be compatible with v1alpha1, we need to add the component from annotation translation here if additionalComponents != nil { desiredComponents = append(desiredComponents, additionalComponents...) From 8b94570e826517def374354762ffa5af4fce4d56 Mon Sep 17 00:00:00 2001 From: wesleysu <59680532+River-sh@users.noreply.github.com> Date: Thu, 31 Aug 2023 10:01:36 +0800 Subject: [PATCH 90/93] add gateway internal service controller (#1677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 珩轩 --- pkg/yurtmanager/controller/controller.go | 7 +- pkg/yurtmanager/controller/raven/common.go | 7 +- .../gateway_internal_service_controller.go | 405 ++++++++++++++++++ ...ateway_internal_service_controller_test.go | 212 +++++++++ ...teway_internal_service_enqueue_handlers.go | 147 +++++++ .../controller/raven/utils/options.go | 68 +++ .../controller/raven/utils/utils.go | 92 +++- 7 files changed, 921 insertions(+), 17 deletions(-) create mode 100644 pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller.go create mode 100644 pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller_test.go create mode 100644 pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_enqueue_handlers.go create mode 100644 pkg/yurtmanager/controller/raven/utils/options.go diff --git a/pkg/yurtmanager/controller/controller.go b/pkg/yurtmanager/controller/controller.go index 297c990dd99..2e8dd41dc39 100644 --- a/pkg/yurtmanager/controller/controller.go +++ b/pkg/yurtmanager/controller/controller.go @@ -28,6 +28,7 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/dns" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewayinternalservice" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" servicetopologyendpoints "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/endpoints" @@ -56,8 +57,6 @@ func init() { controllerAddFuncs[daemonpodupdater.ControllerName] = []AddControllerFn{daemonpodupdater.Add} controllerAddFuncs[delegatelease.ControllerName] = []AddControllerFn{delegatelease.Add} controllerAddFuncs[podbinding.ControllerName] = []AddControllerFn{podbinding.Add} - controllerAddFuncs[raven.GatewayPickupControllerName] = []AddControllerFn{gatewaypickup.Add} - controllerAddFuncs[raven.GatewayDNSControllerName] = []AddControllerFn{dns.Add} controllerAddFuncs[nodepool.ControllerName] = []AddControllerFn{nodepool.Add} controllerAddFuncs[yurtcoordinatorcert.ControllerName] = []AddControllerFn{yurtcoordinatorcert.Add} controllerAddFuncs[servicetopology.ControllerName] = []AddControllerFn{servicetopologyendpoints.Add, servicetopologyendpointslice.Add} @@ -65,6 +64,10 @@ func init() { controllerAddFuncs[yurtappset.ControllerName] = []AddControllerFn{yurtappset.Add} controllerAddFuncs[yurtappdaemon.ControllerName] = []AddControllerFn{yurtappdaemon.Add} controllerAddFuncs[platformadmin.ControllerName] = []AddControllerFn{platformadmin.Add} + + controllerAddFuncs[raven.GatewayPickupControllerName] = []AddControllerFn{gatewaypickup.Add} + controllerAddFuncs[raven.GatewayDNSControllerName] = []AddControllerFn{dns.Add} + controllerAddFuncs[raven.GatewayInternalServiceController] = []AddControllerFn{gatewayinternalservice.Add} } // If you want to add additional RBAC, enter it here !!! @kadisi diff --git a/pkg/yurtmanager/controller/raven/common.go b/pkg/yurtmanager/controller/raven/common.go index 583c3be51e0..7a3b1a59567 100644 --- a/pkg/yurtmanager/controller/raven/common.go +++ b/pkg/yurtmanager/controller/raven/common.go @@ -21,7 +21,8 @@ var ( ) const ( - ControllerName = "gateway" - GatewayPickupControllerName = "raven-gateway-pickup" - GatewayDNSControllerName = "raven-dns" + ControllerName = "gateway" + GatewayPickupControllerName = "raven-gateway-pickup" + GatewayInternalServiceController = "raven-gateway-internal-service" + GatewayDNSControllerName = "raven-dns" ) diff --git a/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller.go b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller.go new file mode 100644 index 00000000000..0f1e7c8946c --- /dev/null +++ b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller.go @@ -0,0 +1,405 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewayinternalservice + +import ( + "context" + "fmt" + "net" + "sort" + "strconv" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +const ( + HTTPPorts = "http" + HTTPSPorts = "https" +) + +func Format(format string, args ...interface{}) string { + s := fmt.Sprintf(format, args...) + return fmt.Sprintf("%s: %s", common.GatewayInternalServiceController, s) +} + +// Add creates a new Service Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { + return add(mgr, newReconciler(c, mgr)) +} + +var _ reconcile.Reconciler = &ReconcileService{} + +// ReconcileService reconciles a Gateway object +type ReconcileService struct { + client.Client + scheme *runtime.Scheme + recorder record.EventRecorder + option utils.Option +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile.Reconciler { + return &ReconcileService{ + Client: mgr.GetClient(), + scheme: mgr.GetScheme(), + recorder: mgr.GetEventRecorderFor(common.GatewayInternalServiceController), + option: utils.NewOption(), + } +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New(common.GatewayInternalServiceController, mgr, controller.Options{ + Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, + }) + if err != nil { + return err + } + + // Watch for changes to Gateway + err = c.Watch(&source.Kind{Type: &ravenv1beta1.Gateway{}}, &EnqueueRequestForGatewayEvent{}) + if err != nil { + return err + } + + //Watch for changes to raven agent + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &EnqueueRequestForConfigEvent{}, predicate.NewPredicateFuncs( + func(object client.Object) bool { + cm, ok := object.(*corev1.ConfigMap) + if !ok { + return false + } + if cm.GetNamespace() != utils.WorkingNamespace { + return false + } + if cm.GetName() != utils.RavenAgentConfig { + return false + } + return true + }, + )) + if err != nil { + return err + } + + return nil +} + +// Reconcile reads that state of the cluster for a Gateway object and makes changes based on the state read +// and what is in the Gateway.Spec +func (r *ReconcileService) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + klog.V(2).Info(Format("started reconciling Service %s/%s", req.Namespace, req.Name)) + defer func() { + klog.V(2).Info(Format("finished reconciling Service %s/%s", req.Namespace, req.Name)) + }() + + gwList, err := r.listExposedGateway(ctx) + if err != nil { + return reconcile.Result{Requeue: true}, err + } + + enableProxy, _ := utils.CheckServer(ctx, r.Client) + r.option.SetProxyOption(enableProxy) + if err := r.reconcileService(ctx, req, gwList); err != nil { + err = fmt.Errorf(Format("unable to reconcile service: %s", err)) + return reconcile.Result{}, err + } + + if err := r.reconcileEndpoint(ctx, req, gwList); err != nil { + err = fmt.Errorf(Format("unable to reconcile endpoint: %s", err)) + return reconcile.Result{}, err + } + return reconcile.Result{}, nil +} + +func (r *ReconcileService) listExposedGateway(ctx context.Context) ([]*ravenv1beta1.Gateway, error) { + var gatewayList ravenv1beta1.GatewayList + if err := r.List(ctx, &gatewayList); err != nil { + return nil, fmt.Errorf(Format("unable to list gateways: %s", err)) + } + exposedGateways := make([]*ravenv1beta1.Gateway, 0) + for _, gw := range gatewayList.Items { + switch gw.Spec.ExposeType { + case ravenv1beta1.ExposeTypePublicIP: + exposedGateways = append(exposedGateways, gw.DeepCopy()) + case ravenv1beta1.ExposeTypeLoadBalancer: + exposedGateways = append(exposedGateways, gw.DeepCopy()) + default: + continue + } + } + return exposedGateways, nil +} + +func (r *ReconcileService) reconcileService(ctx context.Context, req ctrl.Request, gatewayList []*ravenv1beta1.Gateway) error { + if len(gatewayList) == 0 || !r.option.GetProxyOption() { + return r.cleanService(ctx, req) + } + return r.updateService(ctx, req, gatewayList) +} + +func (r *ReconcileService) cleanService(ctx context.Context, req ctrl.Request) error { + if err := r.Delete(ctx, &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: req.Name, + Namespace: req.Namespace, + }, + }); err != nil && !apierrs.IsNotFound(err) { + return err + } + return nil +} + +func generateService(req ctrl.Request) corev1.Service { + return corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: req.Name, + Namespace: req.Namespace, + Labels: map[string]string{ + "app": "raven", + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + }, + } +} + +func (r *ReconcileService) updateService(ctx context.Context, req ctrl.Request, gatewayList []*ravenv1beta1.Gateway) error { + insecurePort, securePort := r.getTargetPort() + servicePorts := acquiredSpecPorts(gatewayList, insecurePort, securePort) + sort.Slice(servicePorts, func(i, j int) bool { + return servicePorts[i].Name < servicePorts[j].Name + }) + + var svc corev1.Service + err := r.Get(ctx, req.NamespacedName, &svc) + if err != nil && !apierrs.IsNotFound(err) { + return err + } + if apierrs.IsNotFound(err) { + klog.V(2).InfoS(Format("create service"), "name", req.Name, "namespace", req.Namespace) + svc = generateService(req) + svc.Spec.Ports = servicePorts + return r.Create(ctx, &svc) + } + svc.Spec.Ports = servicePorts + return r.Update(ctx, &svc) +} + +func (r *ReconcileService) getTargetPort() (insecurePort, securePort int32) { + insecurePort = ravenv1beta1.DefaultProxyServerInsecurePort + securePort = ravenv1beta1.DefaultProxyServerSecurePort + var cm corev1.ConfigMap + err := r.Get(context.TODO(), types.NamespacedName{Namespace: utils.WorkingNamespace, Name: utils.RavenAgentConfig}, &cm) + if err != nil { + return + } + if cm.Data == nil { + return + } + _, internalInsecurePort, err := net.SplitHostPort(cm.Data[utils.ProxyServerInsecurePortKey]) + if err == nil { + insecure, _ := strconv.Atoi(internalInsecurePort) + insecurePort = int32(insecure) + } + + _, internalSecurePort, err := net.SplitHostPort(cm.Data[utils.ProxyServerSecurePortKey]) + if err == nil { + secure, _ := strconv.Atoi(internalSecurePort) + securePort = int32(secure) + } + return +} + +func acquiredSpecPorts(gatewayList []*ravenv1beta1.Gateway, insecurePort, securePort int32) []corev1.ServicePort { + specPorts := make([]corev1.ServicePort, 0) + for _, gw := range gatewayList { + specPorts = append(specPorts, generateServicePorts(gw.Spec.ProxyConfig.ProxyHTTPPort, HTTPPorts, insecurePort)...) + specPorts = append(specPorts, generateServicePorts(gw.Spec.ProxyConfig.ProxyHTTPSPort, HTTPSPorts, securePort)...) + } + return specPorts +} + +func generateServicePorts(ports, namePrefix string, targetPort int32) []corev1.ServicePort { + svcPorts := make([]corev1.ServicePort, 0) + for _, port := range splitPorts(ports) { + p, err := strconv.Atoi(port) + if err != nil { + continue + } + svcPorts = append(svcPorts, corev1.ServicePort{ + Name: fmt.Sprintf("%s-%s", namePrefix, port), + Protocol: corev1.ProtocolTCP, + Port: int32(p), + TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: targetPort}, + }) + } + return svcPorts +} + +func splitPorts(str string) []string { + ret := make([]string, 0) + for _, val := range strings.Split(str, ",") { + ret = append(ret, strings.TrimSpace(val)) + } + return ret +} + +func (r *ReconcileService) reconcileEndpoint(ctx context.Context, req ctrl.Request, gatewayList []*ravenv1beta1.Gateway) error { + var service corev1.Service + err := r.Get(ctx, req.NamespacedName, &service) + if err != nil && !apierrs.IsNotFound(err) { + return err + } + if apierrs.IsNotFound(err) || service.DeletionTimestamp != nil || len(gatewayList) == 0 || !r.option.GetProxyOption() { + return r.cleanEndpoint(ctx, req) + } + return r.updateEndpoint(ctx, req, &service, gatewayList) +} + +func (r *ReconcileService) cleanEndpoint(ctx context.Context, req ctrl.Request) error { + if err := r.Delete(ctx, &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: req.Name, + Namespace: req.Namespace, + }, + }); err != nil && !apierrs.IsNotFound(err) { + return err + } + return nil +} + +func generateEndpoint(req ctrl.Request) corev1.Endpoints { + klog.V(2).InfoS(Format("create endpoint"), "name", req.Name, "namespace", req.Namespace) + return corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: req.Name, + Namespace: req.Namespace, + }, + Subsets: []corev1.EndpointSubset{}, + } +} + +func (r *ReconcileService) updateEndpoint(ctx context.Context, req ctrl.Request, service *corev1.Service, gatewayList []*ravenv1beta1.Gateway) error { + + subsets := []corev1.EndpointSubset{ + { + Addresses: r.ensureSpecEndpoints(ctx, gatewayList), + Ports: ensureSpecPorts(service), + }, + } + if len(subsets[0].Addresses) < 1 || len(subsets[0].Ports) < 1 { + klog.Warning(Format("endpoints %s/%s miss available node address or port, get node %d and port %d", + req.Namespace, req.Name, len(subsets[0].Addresses), len(subsets[0].Ports))) + return nil + } + var eps corev1.Endpoints + err := r.Get(ctx, req.NamespacedName, &eps) + if err != nil && !apierrs.IsNotFound(err) { + return err + } + if apierrs.IsNotFound(err) { + eps = generateEndpoint(req) + eps.Subsets = subsets + return r.Create(ctx, &eps) + } + eps.Subsets = subsets + return r.Update(ctx, &eps) +} + +func (r *ReconcileService) ensureSpecEndpoints(ctx context.Context, gateways []*ravenv1beta1.Gateway) []corev1.EndpointAddress { + specAddresses := make([]corev1.EndpointAddress, 0) + for _, gw := range gateways { + + if len(gw.Status.ActiveEndpoints) < 1 { + newGw, err := r.waitElectEndpoints(ctx, gw.Name) + if err == nil { + gw = newGw + } + } + for _, aep := range gw.Status.ActiveEndpoints { + if aep.Type != ravenv1beta1.Proxy { + continue + } + var node corev1.Node + err := r.Get(ctx, types.NamespacedName{Name: aep.NodeName}, &node) + if err != nil { + continue + } + specAddresses = append(specAddresses, corev1.EndpointAddress{ + IP: utils.GetNodeInternalIP(node), + NodeName: func(n corev1.Node) *string { return &n.Name }(node), + }) + } + } + return specAddresses +} + +func (r *ReconcileService) waitElectEndpoints(ctx context.Context, gwName string) (*ravenv1beta1.Gateway, error) { + var gw ravenv1beta1.Gateway + err := wait.PollImmediate(time.Second*5, time.Minute, func() (done bool, err error) { + err = r.Get(ctx, types.NamespacedName{Name: gwName}, &gw) + if err != nil { + return false, err + } + if len(gw.Status.ActiveEndpoints) < 1 { + return false, nil + } + return true, nil + }) + if err != nil { + return nil, err + } + return gw.DeepCopy(), nil +} + +func ensureSpecPorts(svc *corev1.Service) []corev1.EndpointPort { + specPorts := make([]corev1.EndpointPort, 0) + for _, port := range svc.Spec.Ports { + specPorts = append(specPorts, corev1.EndpointPort{ + Name: port.Name, + Port: int32(port.TargetPort.IntValue()), + Protocol: port.Protocol, + }) + } + return specPorts +} diff --git a/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller_test.go b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller_test.go new file mode 100644 index 00000000000..12166e8b267 --- /dev/null +++ b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller_test.go @@ -0,0 +1,212 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewayinternalservice + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/apis/raven" + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +const ( + Node1Name = "node-1" + Node2Name = "node-2" + Node3Name = "node-3" + Node4Name = "node-4" + Node1Address = "192.168.0.1" + Node2Address = "192.168.0.2" + Node3Address = "192.168.0.3" + Node4Address = "192.168.0.4" + MockGateway = "gw-mock" +) + +func MockReconcile() *ReconcileService { + nodeList := &corev1.NodeList{ + Items: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node1Name, + Labels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + }, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node1Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node2Name, + Labels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + }, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node2Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node3Name, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node3Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node4Name, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node4Address, + }, + }, + }, + }, + }, + } + configmaps := &corev1.ConfigMapList{ + Items: []corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: utils.RavenGlobalConfig, + Namespace: utils.WorkingNamespace, + }, + Data: map[string]string{ + utils.RavenEnableProxy: "true", + utils.RavenEnableTunnel: "true", + }, + }, + }, + } + gateways := &ravenv1beta1.GatewayList{ + Items: []ravenv1beta1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: MockGateway, + }, + Spec: ravenv1beta1.GatewaySpec{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + }, + }, + ProxyConfig: ravenv1beta1.ProxyConfiguration{ + Replicas: 2, + ProxyHTTPPort: "10266,10267,10255,9100", + ProxyHTTPSPort: "10250,9445", + }, + TunnelConfig: ravenv1beta1.TunnelConfiguration{ + Replicas: 1, + }, + Endpoints: []ravenv1beta1.Endpoint{ + { + NodeName: Node1Name, + Type: ravenv1beta1.Proxy, + UnderNAT: false, + }, + { + NodeName: Node2Name, + Type: ravenv1beta1.Proxy, + UnderNAT: false, + }, + }, + ExposeType: ravenv1beta1.ExposeTypeLoadBalancer, + }, + Status: ravenv1beta1.GatewayStatus{ + Nodes: []ravenv1beta1.NodeInfo{ + { + NodeName: Node1Name, + PrivateIP: Node1Address, + }, + { + NodeName: Node2Name, + PrivateIP: Node2Address, + }, + }, + ActiveEndpoints: []*ravenv1beta1.Endpoint{ + { + NodeName: Node1Name, + Type: ravenv1beta1.Proxy, + UnderNAT: false, + }, + { + NodeName: Node2Name, + Type: ravenv1beta1.Proxy, + UnderNAT: false, + }, + }, + }, + }, + }, + } + objs := []runtime.Object{nodeList, gateways, configmaps} + scheme := runtime.NewScheme() + err := clientgoscheme.AddToScheme(scheme) + if err != nil { + return nil + } + err = apis.AddToScheme(scheme) + if err != nil { + return nil + } + + return &ReconcileService{ + Client: fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build(), + recorder: record.NewFakeRecorder(100), + option: utils.NewOption(), + } +} + +func TestReconcileService_Reconcile(t *testing.T) { + r := MockReconcile() + _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Name: utils.GatewayProxyInternalService, Namespace: utils.WorkingNamespace}}) + if err != nil { + t.Errorf("failed to reconcile service %s/%s", utils.WorkingNamespace, utils.GatewayProxyInternalService) + } +} diff --git a/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_enqueue_handlers.go b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_enqueue_handlers.go new file mode 100644 index 00000000000..6efe3157eea --- /dev/null +++ b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_enqueue_handlers.go @@ -0,0 +1,147 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewayinternalservice + +import ( + "net" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/event" + + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +type EnqueueRequestForGatewayEvent struct{} + +func (h *EnqueueRequestForGatewayEvent) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { + gw, ok := e.Object.(*ravenv1beta1.Gateway) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1alpha1.Gateway")) + return + } + if gw.Spec.ExposeType == "" { + return + } + klog.V(2).Infof(Format("enqueue service %s/%s due to gateway %s create event", utils.WorkingNamespace, utils.GatewayProxyInternalService, gw.GetName())) + utils.AddGatewayProxyInternalService(q) +} + +func (h *EnqueueRequestForGatewayEvent) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { + newGw, ok := e.ObjectNew.(*ravenv1beta1.Gateway) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1alpha1.Gateway")) + return + } + oldGw, ok := e.ObjectOld.(*ravenv1beta1.Gateway) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1alpha1.Gateway")) + return + } + if oldGw.Spec.ExposeType == "" && newGw.Spec.ExposeType == "" { + return + } + klog.V(2).Infof(Format("enqueue service %s/%s due to gateway %s update event", utils.WorkingNamespace, utils.GatewayProxyInternalService, newGw.GetName())) + utils.AddGatewayProxyInternalService(q) +} + +func (h *EnqueueRequestForGatewayEvent) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { + gw, ok := e.Object.(*ravenv1beta1.Gateway) + if !ok { + klog.Error(Format("fail to assert runtime Object to v1alpha1.Gateway")) + return + } + if gw.Spec.ExposeType == "" { + return + } + klog.V(2).Infof(Format("enqueue service %s/%s due to gateway %s delete event", utils.WorkingNamespace, utils.GatewayProxyInternalService, gw.GetName())) + utils.AddGatewayProxyInternalService(q) +} + +func (h *EnqueueRequestForGatewayEvent) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { + return +} + +type EnqueueRequestForConfigEvent struct{} + +func (h *EnqueueRequestForConfigEvent) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { + cm, ok := e.Object.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("fail to assert runtime Object to corev1.Configmap")) + return + } + if cm.Data == nil { + return + } + _, _, err := net.SplitHostPort(cm.Data[utils.ProxyServerInsecurePortKey]) + if err == nil { + klog.V(2).Infof(Format("enqueue service %s/%s due to config %s/%s create event", + utils.WorkingNamespace, utils.GatewayProxyInternalService, utils.WorkingNamespace, utils.RavenAgentConfig)) + utils.AddGatewayProxyInternalService(q) + return + } + _, _, err = net.SplitHostPort(cm.Data[utils.ProxyServerSecurePortKey]) + if err == nil { + klog.V(2).Infof(Format("enqueue service %s/%s due to config %s/%s create event", + utils.WorkingNamespace, utils.GatewayProxyInternalService, utils.WorkingNamespace, utils.RavenAgentConfig)) + utils.AddGatewayProxyInternalService(q) + return + } +} + +func (h *EnqueueRequestForConfigEvent) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { + newCm, ok := e.ObjectNew.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("fail to assert runtime Object to corev1.Configmap")) + return + } + oldCm, ok := e.ObjectOld.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("fail to assert runtime Object to corev1.Configmap")) + return + } + _, newInsecurePort, newErr := net.SplitHostPort(newCm.Data[utils.ProxyServerInsecurePortKey]) + _, oldInsecurePort, oldErr := net.SplitHostPort(oldCm.Data[utils.ProxyServerInsecurePortKey]) + if newErr == nil && oldErr == nil { + if newInsecurePort != oldInsecurePort { + klog.V(2).Infof(Format("enqueue service %s/%s due to config %s/%s update event", + utils.WorkingNamespace, utils.GatewayProxyInternalService, utils.WorkingNamespace, utils.RavenAgentConfig)) + utils.AddGatewayProxyInternalService(q) + return + } + } + _, newSecurePort, newErr := net.SplitHostPort(newCm.Data[utils.ProxyServerSecurePortKey]) + _, oldSecurePort, oldErr := net.SplitHostPort(oldCm.Data[utils.ProxyServerSecurePortKey]) + if newErr == nil && oldErr == nil { + if newSecurePort != oldSecurePort { + klog.V(2).Infof(Format("enqueue service %s/%s due to config %s/%s update event", + utils.WorkingNamespace, utils.GatewayProxyInternalService, utils.WorkingNamespace, utils.RavenAgentConfig)) + utils.AddGatewayProxyInternalService(q) + return + } + } +} + +func (h *EnqueueRequestForConfigEvent) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { + return +} + +func (h *EnqueueRequestForConfigEvent) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { + return +} diff --git a/pkg/yurtmanager/controller/raven/utils/options.go b/pkg/yurtmanager/controller/raven/utils/options.go new file mode 100644 index 00000000000..89d244704b3 --- /dev/null +++ b/pkg/yurtmanager/controller/raven/utils/options.go @@ -0,0 +1,68 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import "sync" + +type Option interface { + SetProxyOption(enable bool) + SetTunnelOption(enable bool) + GetProxyOption() bool + GetTunnelOption() bool + Reset() +} + +type ServerOption struct { + mu sync.RWMutex + enableProxy bool + enableTunnel bool +} + +func NewOption() Option { + return &ServerOption{enableTunnel: false, enableProxy: false} +} + +func (o *ServerOption) SetProxyOption(enable bool) { + o.mu.Lock() + defer o.mu.Unlock() + o.enableProxy = enable +} + +func (o *ServerOption) SetTunnelOption(enable bool) { + o.mu.Lock() + defer o.mu.Unlock() + o.enableTunnel = enable +} + +func (o *ServerOption) GetProxyOption() bool { + o.mu.Lock() + defer o.mu.Unlock() + return o.enableProxy +} + +func (o *ServerOption) GetTunnelOption() bool { + o.mu.Lock() + defer o.mu.Unlock() + return o.enableTunnel +} + +func (o *ServerOption) Reset() { + o.mu.Lock() + defer o.mu.Unlock() + o.enableTunnel = false + o.enableProxy = false +} diff --git a/pkg/yurtmanager/controller/raven/utils/utils.go b/pkg/yurtmanager/controller/raven/utils/utils.go index 7219ebde5d8..a5ecdfe9c89 100644 --- a/pkg/yurtmanager/controller/raven/utils/utils.go +++ b/pkg/yurtmanager/controller/raven/utils/utils.go @@ -18,29 +18,46 @@ package utils import ( "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "math/rand" "net" + "strconv" "strings" + "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/openyurtio/openyurt/pkg/apis/raven/v1alpha1" ) const ( WorkingNamespace = "kube-system" RavenGlobalConfig = "raven-cfg" + LabelCurrentGatewayEndpoints = "raven.openyurt.io/endpoints-name" GatewayProxyInternalService = "x-raven-proxy-internal-svc" - GatewayProxyServiceNamePrefix = "x-raven-proxy-svc-" - GatewayTunnelServiceNamePrefix = "x-raven-tunnel-svc-" - RavenProxyNodesConfig = "edge-tunnel-nodes" - ProxyNodesKey = "tunnel-nodes" + GatewayProxyServiceNamePrefix = "x-raven-proxy-svc" + GatewayTunnelServiceNamePrefix = "x-raven-tunnel-svc" + + RavenProxyNodesConfig = "edge-tunnel-nodes" + ProxyNodesKey = "tunnel-nodes" + RavenAgentConfig = "raven-agent-config" + ProxyServerSecurePortKey = "proxy-internal-secure-addr" + ProxyServerInsecurePortKey = "proxy-internal-insecure-addr" + ProxyServerExposedPortKey = "proxy-external-addr" + VPNServerExposedPortKey = "tunnel-bind-addr" + RavenEnableProxy = "EnableL7Proxy" + RavenEnableTunnel = "EnableL3Tunnel" - RavenEnableProxy = "EnableL7Proxy" - RavenEnableTunnel = "EnableL3Tunnel" + KubeletSecurePort = 10250 + KubeletInsecurePort = 10255 + PrometheusSecurePort = 9100 + PrometheusInsecurePort = 9445 ) // GetNodeInternalIP returns internal ip of the given `node`. @@ -55,10 +72,6 @@ func GetNodeInternalIP(node corev1.Node) string { return ip } -func IsGatewayExposeByLB(gateway *v1alpha1.Gateway) bool { - return gateway.Spec.ExposeType == v1alpha1.ExposeTypeLoadBalancer -} - // AddGatewayToWorkQueue adds the Gateway the reconciler's workqueue func AddGatewayToWorkQueue(gwName string, q workqueue.RateLimitingInterface) { @@ -86,8 +99,63 @@ func CheckServer(ctx context.Context, client client.Client) (enableProxy, enable return enableProxy, enableTunnel } +func AddNodePoolToWorkQueue(npName string, q workqueue.RateLimitingInterface) { + if npName != "" { + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{Name: npName}, + }) + } +} + func AddDNSConfigmapToWorkQueue(q workqueue.RateLimitingInterface) { q.Add(reconcile.Request{ NamespacedName: types.NamespacedName{Namespace: WorkingNamespace, Name: RavenProxyNodesConfig}, }) } + +func AddGatewayProxyInternalService(q workqueue.RateLimitingInterface) { + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{Namespace: WorkingNamespace, Name: GatewayProxyInternalService}, + }) +} + +func IsValidPort(s string) bool { + if s == "" { + return false + } + port, err := strconv.Atoi(s) + if err != nil { + return false + } + if port < 0 || port > 65535 { + return false + } + return true +} + +func HashObject(o interface{}) string { + data, _ := json.Marshal(o) + var a interface{} + err := json.Unmarshal(data, &a) + if err != nil { + klog.Errorf("unmarshal: %s", err.Error()) + } + return computeHash(PrettyYaml(a)) +} + +func PrettyYaml(obj interface{}) string { + bs, err := yaml.Marshal(obj) + if err != nil { + klog.Errorf("failed to parse yaml, %v", err.Error()) + } + return string(bs) +} + +func computeHash(target string) string { + hash := sha256.Sum224([]byte(target)) + return strings.ToLower(hex.EncodeToString(hash[:])) +} + +func FormatName(name string) string { + return strings.Join([]string{name, fmt.Sprintf("%08x", rand.Uint32())}, "-") +} From 16e3a3455caf4dcbc6b5a2e9e1ef55a291a10363 Mon Sep 17 00:00:00 2001 From: Liang Deng <283304489@qq.com> Date: Thu, 31 Aug 2023 12:26:36 +0800 Subject: [PATCH 91/93] feat: add token format checking to yurtadm join process (#1681) Signed-off-by: Liang Deng <283304489@qq.com> --- pkg/yurtadm/cmd/join/join.go | 4 ++++ pkg/yurtadm/constants/constants.go | 2 ++ pkg/yurtadm/util/kubernetes/kubernetes.go | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/pkg/yurtadm/cmd/join/join.go b/pkg/yurtadm/cmd/join/join.go index 9e554abb945..89a855e8500 100644 --- a/pkg/yurtadm/cmd/join/join.go +++ b/pkg/yurtadm/cmd/join/join.go @@ -261,6 +261,10 @@ func newJoinData(args []string, opt *joinOptions) (*joinData, error) { return nil, errors.New("join token is empty, so unable to bootstrap worker node.") } + if !yurtadmutil.IsValidBootstrapToken(opt.token) { + return nil, errors.Errorf("the bootstrap token %s was not of the form %s", opt.token, yurtconstants.BootstrapTokenPattern) + } + if opt.nodeType != yurtconstants.EdgeNode && opt.nodeType != yurtconstants.CloudNode { return nil, errors.Errorf("node type(%s) is invalid, only \"edge and cloud\" are supported", opt.nodeType) } diff --git a/pkg/yurtadm/constants/constants.go b/pkg/yurtadm/constants/constants.go index 77f7e63485e..60ae0eb3511 100644 --- a/pkg/yurtadm/constants/constants.go +++ b/pkg/yurtadm/constants/constants.go @@ -57,6 +57,8 @@ const ( KubeletHostname = "--hostname-override=[^\"\\s]*" KubeletEnvironmentFile = "EnvironmentFile=.*" + BootstrapTokenPattern = `\A([a-z0-9]{6})\.([a-z0-9]{16})\z` + DaemonReload = "systemctl daemon-reload" RestartKubeletSvc = "systemctl restart kubelet" diff --git a/pkg/yurtadm/util/kubernetes/kubernetes.go b/pkg/yurtadm/util/kubernetes/kubernetes.go index 17a0c51250b..472c89b33d6 100644 --- a/pkg/yurtadm/util/kubernetes/kubernetes.go +++ b/pkg/yurtadm/util/kubernetes/kubernetes.go @@ -25,6 +25,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "runtime" "strings" "time" @@ -62,6 +63,9 @@ var ( PropagationPolicy = metav1.DeletePropagationBackground ErrClusterVersionEmpty = errors.New("cluster version should not be empty") + + // BootstrapTokenRegexp is a compiled regular expression of TokenRegexpString + BootstrapTokenRegexp = regexp.MustCompile(constants.BootstrapTokenPattern) ) // RunJobAndCleanup runs the job, wait for it to be complete, and delete it @@ -541,3 +545,9 @@ func GetDefaultClientSet() (*kubernetes.Clientset, error) { } return cliSet, nil } + +// IsValidBootstrapToken returns whether the given string is valid as a Bootstrap Token and +// in other words satisfies the BootstrapTokenRegexp +func IsValidBootstrapToken(token string) bool { + return BootstrapTokenRegexp.MatchString(token) +} From 1a5366bd7062b7d9a0e46ded4e3783bfa6679a14 Mon Sep 17 00:00:00 2001 From: wesleysu <59680532+River-sh@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:48:36 +0800 Subject: [PATCH 92/93] add gateway public service controller (#1685) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 珩轩 --- pkg/yurtmanager/controller/controller.go | 2 + pkg/yurtmanager/controller/raven/common.go | 2 +- ...teway_internal_service_enqueue_handlers.go | 14 +- .../gateway_pickup_controller.go | 19 + .../gateway_public_service_controller.go | 695 ++++++++++++++++++ .../gateway_public_service_controller_test.go | 217 ++++++ ...gateway_public_service_enqueue_handlers.go | 163 ++++ .../controller/raven/utils/utils.go | 9 +- pkg/yurtmanager/webhook/server.go | 2 +- 9 files changed, 1107 insertions(+), 16 deletions(-) create mode 100644 pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller.go create mode 100644 pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller_test.go create mode 100644 pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_enqueue_handlers.go diff --git a/pkg/yurtmanager/controller/controller.go b/pkg/yurtmanager/controller/controller.go index 2e8dd41dc39..06f16c73b13 100644 --- a/pkg/yurtmanager/controller/controller.go +++ b/pkg/yurtmanager/controller/controller.go @@ -30,6 +30,7 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/dns" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewayinternalservice" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypublicservice" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" servicetopologyendpoints "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/endpoints" servicetopologyendpointslice "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/endpointslice" @@ -68,6 +69,7 @@ func init() { controllerAddFuncs[raven.GatewayPickupControllerName] = []AddControllerFn{gatewaypickup.Add} controllerAddFuncs[raven.GatewayDNSControllerName] = []AddControllerFn{dns.Add} controllerAddFuncs[raven.GatewayInternalServiceController] = []AddControllerFn{gatewayinternalservice.Add} + controllerAddFuncs[raven.GatewayPublicServiceController] = []AddControllerFn{gatewaypublicservice.Add} } // If you want to add additional RBAC, enter it here !!! @kadisi diff --git a/pkg/yurtmanager/controller/raven/common.go b/pkg/yurtmanager/controller/raven/common.go index 7a3b1a59567..9b94c11f0fc 100644 --- a/pkg/yurtmanager/controller/raven/common.go +++ b/pkg/yurtmanager/controller/raven/common.go @@ -21,8 +21,8 @@ var ( ) const ( - ControllerName = "gateway" GatewayPickupControllerName = "raven-gateway-pickup" GatewayInternalServiceController = "raven-gateway-internal-service" + GatewayPublicServiceController = "raven-gateway-public-service" GatewayDNSControllerName = "raven-dns" ) diff --git a/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_enqueue_handlers.go b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_enqueue_handlers.go index 6efe3157eea..3c888edb4c4 100644 --- a/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_enqueue_handlers.go +++ b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_enqueue_handlers.go @@ -33,7 +33,7 @@ type EnqueueRequestForGatewayEvent struct{} func (h *EnqueueRequestForGatewayEvent) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { gw, ok := e.Object.(*ravenv1beta1.Gateway) if !ok { - klog.Error(Format("fail to assert runtime Object to v1alpha1.Gateway")) + klog.Error(Format("fail to assert runtime Object %s/%s to v1beta1.Gateway", e.Object.GetNamespace(), e.Object.GetName())) return } if gw.Spec.ExposeType == "" { @@ -46,12 +46,12 @@ func (h *EnqueueRequestForGatewayEvent) Create(e event.CreateEvent, q workqueue. func (h *EnqueueRequestForGatewayEvent) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { newGw, ok := e.ObjectNew.(*ravenv1beta1.Gateway) if !ok { - klog.Error(Format("fail to assert runtime Object to v1alpha1.Gateway")) + klog.Error(Format("fail to assert runtime Object %s/%s to v1beta1.Gateway", e.ObjectNew.GetNamespace(), e.ObjectNew.GetName())) return } oldGw, ok := e.ObjectOld.(*ravenv1beta1.Gateway) if !ok { - klog.Error(Format("fail to assert runtime Object to v1alpha1.Gateway")) + klog.Error(Format("fail to assert runtime Object %s/%s to v1beta1.Gateway", e.ObjectOld.GetNamespace(), e.ObjectOld.GetName())) return } if oldGw.Spec.ExposeType == "" && newGw.Spec.ExposeType == "" { @@ -64,7 +64,7 @@ func (h *EnqueueRequestForGatewayEvent) Update(e event.UpdateEvent, q workqueue. func (h *EnqueueRequestForGatewayEvent) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { gw, ok := e.Object.(*ravenv1beta1.Gateway) if !ok { - klog.Error(Format("fail to assert runtime Object to v1alpha1.Gateway")) + klog.Error(Format("fail to assert runtime Object %s/%s to v1beta1.Gateway", e.Object.GetNamespace(), e.Object.GetName())) return } if gw.Spec.ExposeType == "" { @@ -83,7 +83,7 @@ type EnqueueRequestForConfigEvent struct{} func (h *EnqueueRequestForConfigEvent) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { cm, ok := e.Object.(*corev1.ConfigMap) if !ok { - klog.Error(Format("fail to assert runtime Object to corev1.Configmap")) + klog.Error(Format("fail to assert runtime Object %s/%s to v1.Configmap", e.Object.GetNamespace(), e.Object.GetName())) return } if cm.Data == nil { @@ -108,12 +108,12 @@ func (h *EnqueueRequestForConfigEvent) Create(e event.CreateEvent, q workqueue.R func (h *EnqueueRequestForConfigEvent) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { newCm, ok := e.ObjectNew.(*corev1.ConfigMap) if !ok { - klog.Error(Format("fail to assert runtime Object to corev1.Configmap")) + klog.Error(Format("fail to assert runtime Object %s/%s to v1.Configmap", e.ObjectNew.GetNamespace(), e.ObjectNew.GetName())) return } oldCm, ok := e.ObjectOld.(*corev1.ConfigMap) if !ok { - klog.Error(Format("fail to assert runtime Object to corev1.Configmap")) + klog.Error(Format("fail to assert runtime Object %s/%s to v1.Configmap", e.ObjectOld.GetNamespace(), e.ObjectOld.GetName())) return } _, newInsecurePort, newErr := net.SplitHostPort(newCm.Data[utils.ProxyServerInsecurePortKey]) diff --git a/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go index 2449cd775a3..3f56bb8ea0f 100644 --- a/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go +++ b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go @@ -21,6 +21,7 @@ import ( "fmt" "reflect" "sort" + "strconv" "strings" "time" @@ -180,6 +181,7 @@ func (r *ReconcileGateway) Reconcile(ctx context.Context, req reconcile.Request) activeEp := r.electActiveEndpoint(nodeList, &gw) r.recordEndpointEvent(&gw, gw.Status.ActiveEndpoints, activeEp) gw.Status.ActiveEndpoints = activeEp + r.configEndpoints(ctx, &gw) // 2. get nodeInfo list of nodes managed by the Gateway var nodes []ravenv1beta1.NodeInfo for _, v := range nodeList.Items { @@ -363,3 +365,20 @@ func getActiveEndpointsInfo(eps []*ravenv1beta1.Endpoint) (map[string][]string, } return infos, len(infos[ActiveEndpointsName]) } + +func (r *ReconcileGateway) configEndpoints(ctx context.Context, gw *ravenv1beta1.Gateway) { + enableProxy, enableTunnel := utils.CheckServer(ctx, r.Client) + for idx, val := range gw.Status.ActiveEndpoints { + if gw.Status.ActiveEndpoints[idx].Config == nil { + gw.Status.ActiveEndpoints[idx].Config = make(map[string]string) + } + switch val.Type { + case ravenv1beta1.Proxy: + gw.Status.ActiveEndpoints[idx].Config[utils.RavenEnableProxy] = strconv.FormatBool(enableProxy) + case ravenv1beta1.Tunnel: + gw.Status.ActiveEndpoints[idx].Config[utils.RavenEnableTunnel] = strconv.FormatBool(enableTunnel) + default: + } + } + return +} diff --git a/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller.go b/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller.go new file mode 100644 index 00000000000..0bc1d19ed09 --- /dev/null +++ b/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller.go @@ -0,0 +1,695 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewaypublicservice + +import ( + "context" + "fmt" + "net" + "strconv" + "sync" + "time" + + corev1 "k8s.io/api/core/v1" + apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/pkg/apis/raven" + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +const ( + ServiceDeleteFailed = "DeleteServiceFail" +) + +func Format(format string, args ...interface{}) string { + s := fmt.Sprintf(format, args...) + return fmt.Sprintf("%s: %s", common.GatewayPublicServiceController, s) +} + +// Add creates a new Service Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(c *appconfig.CompletedConfig, mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +var _ reconcile.Reconciler = &ReconcileService{} + +type serviceInformation struct { + mu sync.Mutex + data map[string]string +} + +func newServiceInfo() *serviceInformation { + return &serviceInformation{data: make(map[string]string, 0)} +} +func (s *serviceInformation) write(key, val string) { + s.mu.Lock() + defer s.mu.Unlock() + s.data[key] = val +} + +func (s *serviceInformation) read(key string) string { + s.mu.Lock() + defer s.mu.Unlock() + return s.data[key] +} + +func (s *serviceInformation) cleanup() { + s.mu.Lock() + defer s.mu.Unlock() + s.data = make(map[string]string) +} + +// ReconcileService reconciles a Gateway object +type ReconcileService struct { + client.Client + scheme *runtime.Scheme + recorder record.EventRecorder + option utils.Option + svcInfo *serviceInformation +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileService{ + Client: mgr.GetClient(), + scheme: mgr.GetScheme(), + recorder: mgr.GetEventRecorderFor(common.GatewayPublicServiceController), + option: utils.NewOption(), + svcInfo: newServiceInfo(), + } +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New(common.GatewayPublicServiceController, mgr, controller.Options{ + Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, + }) + if err != nil { + return err + } + + // Watch for changes to Gateway + err = c.Watch(&source.Kind{Type: &ravenv1beta1.Gateway{}}, &EnqueueRequestForGatewayEvent{}) + if err != nil { + return err + } + + //Watch for changes to raven agent + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &EnqueueRequestForConfigEvent{client: mgr.GetClient()}, predicate.NewPredicateFuncs( + func(object client.Object) bool { + cm, ok := object.(*corev1.ConfigMap) + if !ok { + return false + } + if cm.GetNamespace() != utils.WorkingNamespace { + return false + } + if cm.GetName() != utils.RavenAgentConfig { + return false + } + return true + }, + )) + if err != nil { + return err + } + return nil +} + +// Reconcile reads that state of the cluster for a Gateway object and makes changes based on the state read +// and what is in the Gateway.Spec +func (r *ReconcileService) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + klog.V(2).Info(Format("started reconciling public service for gateway %s", req.Name)) + defer klog.V(2).Info(Format("finished reconciling public service for gateway %s", req.Name)) + gw, err := r.getGateway(ctx, req) + if err != nil && !apierrs.IsNotFound(err) { + klog.Error(Format("failed to get gateway %s, error %s", req.Name, err.Error())) + return reconcile.Result{}, err + } + if apierrs.IsNotFound(err) { + gw = &ravenv1beta1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: req.Name}} + } + r.svcInfo.cleanup() + r.setOptions(ctx, gw, apierrs.IsNotFound(err)) + if err := r.reconcileService(ctx, gw.DeepCopy()); err != nil { + err = fmt.Errorf(Format("unable to reconcile service: %s", err)) + klog.Error(err.Error()) + return reconcile.Result{Requeue: true, RequeueAfter: 2 * time.Second}, err + } + + if err := r.reconcileEndpoints(ctx, gw.DeepCopy()); err != nil { + err = fmt.Errorf(Format("unable to reconcile endpoint: %s", err)) + return reconcile.Result{Requeue: true, RequeueAfter: 2 * time.Second}, err + } + return reconcile.Result{}, nil +} + +func (r *ReconcileService) setOptions(ctx context.Context, gw *ravenv1beta1.Gateway, isNotFound bool) { + r.option.SetProxyOption(true) + r.option.SetTunnelOption(true) + if isNotFound { + r.option.SetProxyOption(false) + r.option.SetTunnelOption(false) + klog.V(4).Info(Format("set option for proxy (%t) and tunnel (%t), reason gateway %s is not found", + false, false, gw.GetName())) + return + } + + if gw.DeletionTimestamp != nil { + r.option.SetProxyOption(false) + r.option.SetTunnelOption(false) + klog.V(4).Info(Format("set option for proxy (%t) and tunnel (%t), reason: gateway %s is deleted ", + false, false, gw.GetName())) + return + } + + if gw.Spec.ExposeType != ravenv1beta1.ExposeTypeLoadBalancer { + r.option.SetProxyOption(false) + r.option.SetTunnelOption(false) + klog.V(4).Info(Format("set option for proxy (%t) and tunnel (%t), reason: gateway %s exposed type is %s ", + false, false, gw.GetName(), gw.Spec.ExposeType)) + return + } + + enableProxy, enableTunnel := utils.CheckServer(ctx, r.Client) + if !enableTunnel { + r.option.SetTunnelOption(enableTunnel) + klog.V(4).Info(Format("set option for tunnel (%t), reason: raven-cfg close tunnel ", false)) + } + + if !enableProxy { + r.option.SetProxyOption(enableProxy) + klog.V(4).Info(Format("set option for tunnel (%t), reason: raven-cfg close proxy ", false)) + } + return +} + +func (r *ReconcileService) getGateway(ctx context.Context, req reconcile.Request) (*ravenv1beta1.Gateway, error) { + var gw ravenv1beta1.Gateway + err := r.Get(ctx, req.NamespacedName, &gw) + if err != nil { + return nil, err + } + return gw.DeepCopy(), nil +} + +func (r *ReconcileService) generateServiceName(services []corev1.Service) { + for _, svc := range services { + epName := svc.Labels[utils.LabelCurrentGatewayEndpoints] + epType := svc.Labels[raven.LabelCurrentGatewayType] + if epName == "" || epType == "" { + continue + } + r.svcInfo.write(formatKey(epName, epType), svc.GetName()) + } + return +} + +func (r *ReconcileService) reconcileService(ctx context.Context, gw *ravenv1beta1.Gateway) error { + enableProxy := r.option.GetProxyOption() + if enableProxy { + klog.V(2).Info(Format("start manage proxy service for gateway %s", gw.GetName())) + defer klog.V(2).Info(Format("finish manage proxy service for gateway %s", gw.GetName())) + if err := r.manageService(ctx, gw, ravenv1beta1.Proxy); err != nil { + return fmt.Errorf("failed to manage service for proxy server %s", err.Error()) + } + } else { + klog.V(2).Info(Format("start clear proxy service for gateway %s", gw.GetName())) + defer klog.V(2).Info(Format("finish clear proxy service for gateway %s", gw.GetName())) + if err := r.clearService(ctx, gw.GetName(), ravenv1beta1.Proxy); err != nil { + return fmt.Errorf("failed to clear service for proxy server %s", err.Error()) + } + } + + enableTunnel := r.option.GetTunnelOption() + if enableTunnel { + klog.V(2).Info(Format("start manage tunnel service for gateway %s", gw.GetName())) + defer klog.V(2).Info(Format("finish manage tunnel service for gateway %s", gw.GetName())) + if err := r.manageService(ctx, gw, ravenv1beta1.Tunnel); err != nil { + return fmt.Errorf("failed to manage service for tunnel server %s", err.Error()) + } + } else { + klog.V(2).Info(Format("start clear tunnel service for gateway %s", gw.GetName())) + defer klog.V(2).Info(Format("finish clear tunnel service for gateway %s", gw.GetName())) + if err := r.clearService(ctx, gw.GetName(), ravenv1beta1.Tunnel); err != nil { + return fmt.Errorf("failed to clear service for tunnel server %s", err.Error()) + } + } + return nil +} + +func (r *ReconcileService) reconcileEndpoints(ctx context.Context, gw *ravenv1beta1.Gateway) error { + enableProxy := r.option.GetProxyOption() + if enableProxy { + klog.V(2).Info(Format("start manage proxy service endpoints for gateway %s", gw.GetName())) + defer klog.V(2).Info(Format("finish manage proxy service endpoints for gateway %s", gw.GetName())) + if err := r.manageEndpoints(ctx, gw, ravenv1beta1.Proxy); err != nil { + return fmt.Errorf("failed to manage endpoints for proxy server %s", err.Error()) + } + } else { + klog.V(2).Info(Format("start clear proxy service endpoints for gateway %s", gw.GetName())) + defer klog.V(2).Info(Format("finish clear proxy service endpoints for gateway %s", gw.GetName())) + if err := r.clearEndpoints(ctx, gw.GetName(), ravenv1beta1.Proxy); err != nil { + return fmt.Errorf("failed to clear endpoints for proxy server %s", err.Error()) + } + } + enableTunnel := r.option.GetTunnelOption() + if enableTunnel { + klog.V(2).Info(Format("start manage tunnel service endpoints for gateway %s", gw.GetName())) + defer klog.V(2).Info(Format("finish manage tunnel service endpoints for gateway %s", gw.GetName())) + if err := r.manageEndpoints(ctx, gw, ravenv1beta1.Tunnel); err != nil { + return fmt.Errorf("failed to manage endpoints for tunnel server %s", err.Error()) + } + } else { + klog.V(2).Info(Format("start clear tunnel service endpoints for gateway %s", gw.GetName())) + defer klog.V(2).Info(Format("finish clear tunnel service endpoints for gateway %s", gw.GetName())) + if err := r.clearEndpoints(ctx, gw.GetName(), ravenv1beta1.Tunnel); err != nil { + return fmt.Errorf("failed to clear endpoints for tunnel server %s", err.Error()) + } + } + return nil +} + +func (r *ReconcileService) clearService(ctx context.Context, gatewayName, gatewayType string) error { + svcList, err := r.listService(ctx, gatewayName, gatewayType) + if err != nil { + return fmt.Errorf("failed to list service for gateway %s", gatewayName) + } + for _, svc := range svcList.Items { + err := r.Delete(ctx, svc.DeepCopy()) + if err != nil { + r.recorder.Event(svc.DeepCopy(), corev1.EventTypeWarning, ServiceDeleteFailed, + fmt.Sprintf("The gateway %s %s server is not need to exposed by loadbalancer, failed to delete service %s/%s", + gatewayName, gatewayType, svc.GetNamespace(), svc.GetName())) + continue + } + } + return nil +} + +func (r *ReconcileService) clearEndpoints(ctx context.Context, gatewayName, gatewayType string) error { + epsList, err := r.listEndpoints(ctx, gatewayName, gatewayType) + if err != nil { + return fmt.Errorf("failed to list endpoints for gateway %s", gatewayName) + } + for _, eps := range epsList.Items { + err := r.Delete(ctx, eps.DeepCopy()) + if err != nil { + r.recorder.Event(eps.DeepCopy(), corev1.EventTypeWarning, ServiceDeleteFailed, + fmt.Sprintf("The gateway %s %s server is not need to exposed by loadbalancer, failed to delete endpoints %s/%s", + gatewayName, gatewayType, eps.GetNamespace(), eps.GetName())) + continue + } + } + return nil +} + +func (r *ReconcileService) manageService(ctx context.Context, gateway *ravenv1beta1.Gateway, gatewayType string) error { + curSvcList, err := r.listService(ctx, gateway.GetName(), gatewayType) + if err != nil { + return fmt.Errorf("failed list service for gateway %s type %s , error %s", gateway.GetName(), gatewayType, err.Error()) + } + proxyPort, tunnelPort := r.getTargetPort() + specSvcList := acquiredSpecService(gateway, gatewayType, proxyPort, tunnelPort) + addSvc, updateSvc, deleteSvc := classifyService(curSvcList, specSvcList) + r.generateServiceName(specSvcList.Items) + for i := 0; i < len(addSvc); i++ { + if err := r.Create(ctx, addSvc[i]); err != nil { + if apierrs.IsAlreadyExists(err) { + klog.V(2).Info(Format("service %s/%s has already exist, ignore creating it", addSvc[i].GetNamespace(), addSvc[i].GetName())) + return nil + } + return fmt.Errorf("failed create service for gateway %s type %s , error %s", gateway.GetName(), gatewayType, err.Error()) + } + } + for i := 0; i < len(updateSvc); i++ { + if err := r.Update(ctx, updateSvc[i]); err != nil { + return fmt.Errorf("failed update service for gateway %s type %s , error %s", gateway.GetName(), gatewayType, err.Error()) + } + } + for i := 0; i < len(deleteSvc); i++ { + if err := r.Delete(ctx, deleteSvc[i]); err != nil { + return fmt.Errorf("failed delete service for gateway %s type %s , error %s", gateway.GetName(), gatewayType, err.Error()) + } + } + return nil +} + +func (r *ReconcileService) manageEndpoints(ctx context.Context, gateway *ravenv1beta1.Gateway, gatewayType string) error { + currEpsList, err := r.listEndpoints(ctx, gateway.GetName(), gatewayType) + if err != nil { + return fmt.Errorf("failed list service for gateway %s type %s , error %s", gateway.GetName(), gatewayType, err.Error()) + } + specEpsList := r.acquiredSpecEndpoints(ctx, gateway, gatewayType) + addEps, updateEps, deleteEps := classifyEndpoints(currEpsList, specEpsList) + for i := 0; i < len(addEps); i++ { + if err := r.Create(ctx, addEps[i]); err != nil { + if apierrs.IsAlreadyExists(err) { + klog.V(2).Info(Format("endpoints %s/%s has already exist, ignore creating it", addEps[i].GetNamespace(), addEps[i].GetName())) + return nil + } + return fmt.Errorf("failed create endpoints for gateway %s type %s , error %s", gateway.GetName(), gatewayType, err.Error()) + } + } + for i := 0; i < len(updateEps); i++ { + if err := r.Update(ctx, updateEps[i]); err != nil { + return fmt.Errorf("failed update endpoints for gateway %s type %s , error %s", gateway.GetName(), gatewayType, err.Error()) + } + } + for i := 0; i < len(deleteEps); i++ { + if err := r.Delete(ctx, deleteEps[i]); err != nil { + return fmt.Errorf("failed delete endpoints for gateway %s type %s , error %s", gateway.GetName(), gatewayType, err.Error()) + } + } + return nil +} + +func (r *ReconcileService) getTargetPort() (proxyPort, tunnelPort int32) { + proxyPort = ravenv1beta1.DefaultProxyServerExposedPort + tunnelPort = ravenv1beta1.DefaultTunnelServerExposedPort + var cm corev1.ConfigMap + err := r.Get(context.TODO(), types.NamespacedName{Namespace: utils.WorkingNamespace, Name: utils.RavenAgentConfig}, &cm) + if err != nil { + return + } + if cm.Data == nil { + return + } + _, proxyExposedPort, err := net.SplitHostPort(cm.Data[utils.ProxyServerExposedPortKey]) + if err == nil { + proxy, _ := strconv.Atoi(proxyExposedPort) + proxyPort = int32(proxy) + } + _, tunnelExposedPort, err := net.SplitHostPort(cm.Data[utils.VPNServerExposedPortKey]) + if err == nil { + tunnel, _ := strconv.Atoi(tunnelExposedPort) + tunnelPort = int32(tunnel) + } + return +} + +func (r *ReconcileService) listService(ctx context.Context, gatewayName, gatewayType string) (*corev1.ServiceList, error) { + var svcList corev1.ServiceList + err := r.List(ctx, &svcList, &client.ListOptions{ + LabelSelector: labels.Set{ + raven.LabelCurrentGateway: gatewayName, + raven.LabelCurrentGatewayType: gatewayType, + }.AsSelector(), + }) + if err != nil { + return nil, err + } + newList := make([]corev1.Service, 0) + for _, val := range svcList.Items { + if val.Spec.Type == corev1.ServiceTypeLoadBalancer { + newList = append(newList, val) + } + } + svcList.Items = newList + return &svcList, nil +} + +func (r *ReconcileService) listEndpoints(ctx context.Context, gatewayName, gatewayType string) (*corev1.EndpointsList, error) { + var epsList corev1.EndpointsList + err := r.List(ctx, &epsList, &client.ListOptions{ + LabelSelector: labels.Set{ + raven.LabelCurrentGateway: gatewayName, + raven.LabelCurrentGatewayType: gatewayType, + }.AsSelector()}) + if err != nil { + return nil, err + } + return &epsList, nil +} + +func (r *ReconcileService) acquiredSpecEndpoints(ctx context.Context, gateway *ravenv1beta1.Gateway, gatewayType string) *corev1.EndpointsList { + proxyPort, tunnelPort := r.getTargetPort() + endpoints := make([]corev1.Endpoints, 0) + for _, aep := range gateway.Status.ActiveEndpoints { + if aep.Type != gatewayType { + continue + } + address, err := r.getEndpointsAddress(ctx, aep.NodeName) + if err != nil { + continue + } + switch aep.Type { + case ravenv1beta1.Proxy: + name := r.svcInfo.read(formatKey(aep.NodeName, ravenv1beta1.Proxy)) + if name == "" { + continue + } + endpoints = append(endpoints, corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: utils.WorkingNamespace, + Labels: map[string]string{ + raven.LabelCurrentGateway: gateway.GetName(), + raven.LabelCurrentGatewayType: ravenv1beta1.Proxy, + utils.LabelCurrentGatewayEndpoints: aep.NodeName, + }, + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{*address}, + Ports: []corev1.EndpointPort{ + { + Port: proxyPort, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + }, + }) + case ravenv1beta1.Tunnel: + name := r.svcInfo.read(formatKey(aep.NodeName, ravenv1beta1.Tunnel)) + if name == "" { + continue + } + endpoints = append(endpoints, corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: utils.WorkingNamespace, + Labels: map[string]string{ + raven.LabelCurrentGateway: gateway.GetName(), + raven.LabelCurrentGatewayType: ravenv1beta1.Tunnel, + utils.LabelCurrentGatewayEndpoints: aep.NodeName, + }, + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{*address}, + Ports: []corev1.EndpointPort{ + { + Port: tunnelPort, + Protocol: corev1.ProtocolUDP, + }, + }, + }, + }, + }) + } + } + return &corev1.EndpointsList{Items: endpoints} +} + +func (r *ReconcileService) getEndpointsAddress(ctx context.Context, name string) (*corev1.EndpointAddress, error) { + var node corev1.Node + err := r.Get(ctx, types.NamespacedName{Name: name}, &node) + if err != nil { + klog.Errorf(Format("failed to get node %s for get active endpoints address, error %s", name, err.Error())) + return nil, err + } + return &corev1.EndpointAddress{NodeName: func(n corev1.Node) *string { return &n.Name }(node), IP: utils.GetNodeInternalIP(node)}, nil +} + +func acquiredSpecService(gateway *ravenv1beta1.Gateway, gatewayType string, proxyPort, tunnelPort int32) *corev1.ServiceList { + services := make([]corev1.Service, 0) + if gateway == nil { + return &corev1.ServiceList{Items: services} + } + for _, aep := range gateway.Status.ActiveEndpoints { + if aep.Type != gatewayType { + continue + } + if aep.Port < 1 || aep.Port > 65535 { + continue + } + switch aep.Type { + case ravenv1beta1.Proxy: + services = append(services, corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.FormatName(fmt.Sprintf("%s-%s", utils.GatewayProxyServiceNamePrefix, gateway.GetName())), + Namespace: utils.WorkingNamespace, + Labels: map[string]string{ + raven.LabelCurrentGateway: gateway.GetName(), + raven.LabelCurrentGatewayType: ravenv1beta1.Proxy, + utils.LabelCurrentGatewayEndpoints: aep.NodeName, + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal, + Ports: []corev1.ServicePort{ + { + Protocol: corev1.ProtocolTCP, + Port: int32(aep.Port), + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: proxyPort, + }, + }, + }, + }, + }) + case ravenv1beta1.Tunnel: + services = append(services, corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.FormatName(fmt.Sprintf("%s-%s", utils.GatewayTunnelServiceNamePrefix, gateway.GetName())), + Namespace: utils.WorkingNamespace, + Labels: map[string]string{ + raven.LabelCurrentGateway: gateway.GetName(), + raven.LabelCurrentGatewayType: ravenv1beta1.Tunnel, + utils.LabelCurrentGatewayEndpoints: aep.NodeName, + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal, + Ports: []corev1.ServicePort{ + { + Protocol: corev1.ProtocolUDP, + Port: int32(aep.Port), + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: tunnelPort, + }, + }, + }, + }, + }) + } + } + return &corev1.ServiceList{Items: services} +} + +func classifyService(current, spec *corev1.ServiceList) (added, updated, deleted []*corev1.Service) { + added = make([]*corev1.Service, 0) + updated = make([]*corev1.Service, 0) + deleted = make([]*corev1.Service, 0) + + getKey := func(svc *corev1.Service) string { + epType := svc.Labels[raven.LabelCurrentGatewayType] + epName := svc.Labels[utils.LabelCurrentGatewayEndpoints] + if epType == "" { + return "" + } + if epName == "" { + return "" + } + return formatKey(epName, epType) + } + + r := make(map[string]int) + for idx, val := range current.Items { + if key := getKey(&val); key != "" { + r[key] = idx + } + } + for _, val := range spec.Items { + if key := getKey(&val); key != "" { + if idx, ok := r[key]; ok { + updatedService := current.Items[idx].DeepCopy() + updatedService.Spec = *val.Spec.DeepCopy() + updated = append(updated, updatedService) + delete(r, key) + } else { + added = append(added, val.DeepCopy()) + } + } + + } + for key, val := range r { + deleted = append(deleted, current.Items[val].DeepCopy()) + delete(r, key) + } + return added, updated, deleted +} + +func classifyEndpoints(current, spec *corev1.EndpointsList) (added, updated, deleted []*corev1.Endpoints) { + added = make([]*corev1.Endpoints, 0) + updated = make([]*corev1.Endpoints, 0) + deleted = make([]*corev1.Endpoints, 0) + + getKey := func(ep *corev1.Endpoints) string { + epType := ep.Labels[raven.LabelCurrentGatewayType] + epName := ep.Labels[utils.LabelCurrentGatewayEndpoints] + if epType == "" { + return "" + } + if epName == "" { + return "" + } + return formatKey(epName, epType) + } + + r := make(map[string]int) + for idx, val := range current.Items { + if key := getKey(&val); key != "" { + r[key] = idx + } + } + for _, val := range spec.Items { + if key := getKey(&val); key != "" { + if idx, ok := r[key]; ok { + updatedEndpoints := current.Items[idx].DeepCopy() + updatedEndpoints.Subsets = val.DeepCopy().Subsets + updated = append(updated, updatedEndpoints) + delete(r, key) + } else { + added = append(added, val.DeepCopy()) + } + } + } + for key, val := range r { + deleted = append(deleted, current.Items[val].DeepCopy()) + delete(r, key) + } + return added, updated, deleted +} + +func formatKey(endpointName, endpointType string) string { + return fmt.Sprintf("%s-%s", endpointName, endpointType) +} diff --git a/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller_test.go b/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller_test.go new file mode 100644 index 00000000000..6a9e6de30ce --- /dev/null +++ b/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller_test.go @@ -0,0 +1,217 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewaypublicservice + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/apis/raven" + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +const ( + Node1Name = "node-1" + Node2Name = "node-2" + Node3Name = "node-3" + Node4Name = "node-4" + Node1Address = "192.168.0.1" + Node2Address = "192.168.0.2" + Node3Address = "192.168.0.3" + Node4Address = "192.168.0.4" + MockGateway = "gw-mock" +) + +func MockReconcile() *ReconcileService { + nodeList := &corev1.NodeList{ + Items: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node1Name, + Labels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + }, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node1Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node2Name, + Labels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + }, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node2Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node3Name, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node3Address, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: Node4Name, + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: Node4Address, + }, + }, + }, + }, + }, + } + configmaps := &corev1.ConfigMapList{ + Items: []corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: utils.RavenGlobalConfig, + Namespace: utils.WorkingNamespace, + }, + Data: map[string]string{ + utils.RavenEnableProxy: "true", + utils.RavenEnableTunnel: "true", + }, + }, + }, + } + gateways := &ravenv1beta1.GatewayList{ + Items: []ravenv1beta1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: MockGateway, + }, + Spec: ravenv1beta1.GatewaySpec{ + NodeSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + raven.LabelCurrentGateway: MockGateway, + }, + }, + ProxyConfig: ravenv1beta1.ProxyConfiguration{ + Replicas: 2, + ProxyHTTPPort: "10266,10267,10255,9100", + ProxyHTTPSPort: "10250,9445", + }, + TunnelConfig: ravenv1beta1.TunnelConfiguration{ + Replicas: 1, + }, + Endpoints: []ravenv1beta1.Endpoint{ + { + NodeName: Node1Name, + Type: ravenv1beta1.Proxy, + Port: ravenv1beta1.DefaultProxyServerExposedPort, + UnderNAT: false, + }, + { + NodeName: Node2Name, + Type: ravenv1beta1.Proxy, + Port: ravenv1beta1.DefaultProxyServerExposedPort, + UnderNAT: false, + }, + }, + ExposeType: ravenv1beta1.ExposeTypeLoadBalancer, + }, + Status: ravenv1beta1.GatewayStatus{ + Nodes: []ravenv1beta1.NodeInfo{ + { + NodeName: Node1Name, + PrivateIP: Node1Address, + }, + { + NodeName: Node2Name, + PrivateIP: Node2Address, + }, + }, + ActiveEndpoints: []*ravenv1beta1.Endpoint{ + { + NodeName: Node1Name, + Type: ravenv1beta1.Proxy, + Port: ravenv1beta1.DefaultProxyServerExposedPort, + UnderNAT: false, + }, + { + NodeName: Node2Name, + Type: ravenv1beta1.Proxy, + Port: ravenv1beta1.DefaultProxyServerExposedPort, + UnderNAT: false, + }, + }, + }, + }, + }, + } + objs := []runtime.Object{nodeList, gateways, configmaps} + scheme := runtime.NewScheme() + err := clientgoscheme.AddToScheme(scheme) + if err != nil { + return nil + } + err = apis.AddToScheme(scheme) + if err != nil { + return nil + } + + return &ReconcileService{ + Client: fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build(), + recorder: record.NewFakeRecorder(100), + option: utils.NewOption(), + svcInfo: newServiceInfo(), + } +} + +func TestReconcileService_Reconcile(t *testing.T) { + r := MockReconcile() + _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Name: MockGateway}}) + if err != nil { + t.Errorf("failed to reconcile service %s", MockGateway) + } +} diff --git a/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_enqueue_handlers.go b/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_enqueue_handlers.go new file mode 100644 index 00000000000..2185365f0ac --- /dev/null +++ b/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_enqueue_handlers.go @@ -0,0 +1,163 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the License); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an AS IS BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gatewaypublicservice + +import ( + "context" + "net" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + + ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" +) + +type EnqueueRequestForGatewayEvent struct{} + +func (h *EnqueueRequestForGatewayEvent) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { + gw, ok := e.Object.(*ravenv1beta1.Gateway) + if !ok { + klog.Error(Format("fail to assert runtime Object %s/%s to v1beta1.Gateway,", e.Object.GetNamespace(), e.Object.GetName())) + return + } + if gw.Spec.ExposeType == "" { + return + } + klog.V(2).Infof(Format("enqueue gateway %s as create event", gw.GetName())) + utils.AddGatewayToWorkQueue(gw.GetName(), q) +} + +func (h *EnqueueRequestForGatewayEvent) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { + newGw, ok := e.ObjectNew.(*ravenv1beta1.Gateway) + if !ok { + klog.Error(Format("fail to assert runtime Object %s/%s to v1beta1.Gateway,", e.ObjectNew.GetNamespace(), e.ObjectNew.GetName())) + return + } + oldGw, ok := e.ObjectOld.(*ravenv1beta1.Gateway) + if !ok { + klog.Error(Format("fail to assert runtime Object %s/%s to v1beta1.Gateway,", e.ObjectOld.GetNamespace(), e.ObjectOld.GetName())) + return + } + if needUpdate(newGw, oldGw) { + klog.V(2).Infof(Format("enqueue gateway %s as update event", newGw.GetName())) + utils.AddGatewayToWorkQueue(newGw.GetName(), q) + } +} + +func (h *EnqueueRequestForGatewayEvent) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { + gw, ok := e.Object.(*ravenv1beta1.Gateway) + if !ok { + klog.Error(Format("fail to assert runtime Object %s/%s to v1beta1.Gateway,", e.Object.GetNamespace(), e.Object.GetName())) + return + } + if gw.Spec.ExposeType == "" { + return + } + if gw.DeletionTimestamp != nil { + return + } + klog.V(2).Infof(Format("enqueue gateway %s as delete event", gw.GetName())) + utils.AddGatewayToWorkQueue(gw.GetName(), q) +} + +func (h *EnqueueRequestForGatewayEvent) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { + return +} + +func needUpdate(newObj, oldObj *ravenv1beta1.Gateway) bool { + if newObj.Spec.ExposeType != oldObj.Spec.ExposeType { + return true + } + if utils.HashObject(newObj.Status.ActiveEndpoints) != utils.HashObject(oldObj.Status.ActiveEndpoints) { + return true + } + return false +} + +type EnqueueRequestForConfigEvent struct { + client client.Client +} + +func (h *EnqueueRequestForConfigEvent) Create(e event.CreateEvent, q workqueue.RateLimitingInterface) { + cm, ok := e.Object.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("fail to assert runtime Object %s/%s to v1.Configmap,", e.Object.GetNamespace(), e.Object.GetName())) + return + } + if cm.Data == nil { + return + } + if _, _, err := net.SplitHostPort(cm.Data[utils.ProxyServerExposedPortKey]); err == nil { + h.addExposedGateway(q) + return + } + if _, _, err := net.SplitHostPort(cm.Data[utils.VPNServerExposedPortKey]); err == nil { + h.addExposedGateway(q) + return + } +} + +func (h *EnqueueRequestForConfigEvent) Update(e event.UpdateEvent, q workqueue.RateLimitingInterface) { + newCm, ok := e.ObjectNew.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("fail to assert runtime Object %s/%s to v1.Configmap,", e.ObjectNew.GetNamespace(), e.ObjectNew.GetName())) + return + } + oldCm, ok := e.ObjectOld.(*corev1.ConfigMap) + if !ok { + klog.Error(Format("fail to assert runtime Object %s/%s to v1.Configmap,", e.ObjectOld.GetNamespace(), e.ObjectOld.GetName())) + return + } + _, newProxyPort, newErr := net.SplitHostPort(newCm.Data[utils.ProxyServerExposedPortKey]) + _, oldProxyPort, oldErr := net.SplitHostPort(oldCm.Data[utils.ProxyServerExposedPortKey]) + if newErr == nil && oldErr == nil && newProxyPort != oldProxyPort { + h.addExposedGateway(q) + return + } + _, newTunnelPort, newErr := net.SplitHostPort(newCm.Data[utils.VPNServerExposedPortKey]) + _, oldTunnelPort, oldErr := net.SplitHostPort(oldCm.Data[utils.VPNServerExposedPortKey]) + if newErr == nil && oldErr == nil && newTunnelPort != oldTunnelPort { + h.addExposedGateway(q) + return + } +} + +func (h *EnqueueRequestForConfigEvent) Delete(e event.DeleteEvent, q workqueue.RateLimitingInterface) { + return +} + +func (h *EnqueueRequestForConfigEvent) Generic(e event.GenericEvent, q workqueue.RateLimitingInterface) { + return +} + +func (h *EnqueueRequestForConfigEvent) addExposedGateway(q workqueue.RateLimitingInterface) { + var gwList ravenv1beta1.GatewayList + err := h.client.List(context.TODO(), &gwList) + if err != nil { + return + } + for _, gw := range gwList.Items { + if gw.Spec.ExposeType == ravenv1beta1.ExposeTypeLoadBalancer { + klog.V(2).Infof(Format("enqueue gateway %s", gw.GetName())) + utils.AddGatewayToWorkQueue(gw.GetName(), q) + } + } +} diff --git a/pkg/yurtmanager/controller/raven/utils/utils.go b/pkg/yurtmanager/controller/raven/utils/utils.go index a5ecdfe9c89..b47ad097f41 100644 --- a/pkg/yurtmanager/controller/raven/utils/utils.go +++ b/pkg/yurtmanager/controller/raven/utils/utils.go @@ -51,13 +51,8 @@ const ( ProxyServerInsecurePortKey = "proxy-internal-insecure-addr" ProxyServerExposedPortKey = "proxy-external-addr" VPNServerExposedPortKey = "tunnel-bind-addr" - RavenEnableProxy = "EnableL7Proxy" - RavenEnableTunnel = "EnableL3Tunnel" - - KubeletSecurePort = 10250 - KubeletInsecurePort = 10255 - PrometheusSecurePort = 9100 - PrometheusInsecurePort = 9445 + RavenEnableProxy = "enable-l7-proxy" + RavenEnableTunnel = "enable-l3-tunnel" ) // GetNodeInternalIP returns internal ip of the given `node`. diff --git a/pkg/yurtmanager/webhook/server.go b/pkg/yurtmanager/webhook/server.go index a57ed20f149..47547c10a26 100644 --- a/pkg/yurtmanager/webhook/server.go +++ b/pkg/yurtmanager/webhook/server.go @@ -73,7 +73,7 @@ func addControllerWebhook(name string, handler SetupWebhookWithManager) { } func init() { - addControllerWebhook(raven.ControllerName, &v1beta1gateway.GatewayHandler{}) + addControllerWebhook(raven.GatewayPickupControllerName, &v1beta1gateway.GatewayHandler{}) addControllerWebhook(nodepool.ControllerName, &v1beta1nodepool.NodePoolHandler{}) addControllerWebhook(yurtstaticset.ControllerName, &v1alpha1yurtstaticset.YurtStaticSetHandler{}) addControllerWebhook(yurtappset.ControllerName, &v1alpha1yurtappset.YurtAppSetHandler{}) From 8b8c6e11fc5bfc53bf38f8c9c05b48abfca46145 Mon Sep 17 00:00:00 2001 From: rambohe Date: Mon, 4 Sep 2023 09:49:36 +0800 Subject: [PATCH 93/93] improve controller names (#1687) --- charts/yurt-manager/values.yaml | 2 +- cmd/yurt-manager/app/manager.go | 5 +- cmd/yurt-manager/app/options/generic.go | 48 ++++++--- cmd/yurt-manager/app/options/options.go | 18 ++-- cmd/yurt-manager/names/controller_names.go | 58 +++++++++++ go.mod | 1 + go.sum | 1 + pkg/yurtmanager/controller/controller.go | 98 +++++++++++-------- .../csrapprover/csrapprover_controller.go | 7 +- .../daemon_pod_updater_controller.go | 9 +- .../nodepool/nodepool_controller.go | 11 +-- .../platformadmin/platformadmin_controller.go | 9 +- pkg/yurtmanager/controller/raven/common.go | 7 -- .../controller/raven/dns/dns_controller.go | 7 +- .../gateway_internal_service_controller.go | 7 +- .../gateway_pickup_controller.go | 7 +- .../gateway_public_service_controller.go | 7 +- .../controller/servicetopology/common.go | 21 ---- .../endpoints/endpoints_controller.go | 6 +- .../endpointslice/endpointslice_controller.go | 8 +- .../controller/util/controller_utils.go | 34 ------- .../controller/util/controller_utils_test.go | 57 ----------- .../yurtappdaemon/yurtappdaemon_controller.go | 13 ++- .../yurtappdaemon_controller_test.go | 9 +- .../yurtappset/yurtappset_controller.go | 9 +- .../cert/yurtcoordinatorcert_controller.go | 7 +- .../delegatelease/delegatelease_controller.go | 7 +- .../podbinding/podbinding_controller.go | 9 +- .../yurtstaticset/yurtstaticset_controller.go | 9 +- pkg/yurtmanager/webhook/server.go | 26 +++-- test/e2e/cmd/init/constants/constants.go | 1 + 31 files changed, 244 insertions(+), 274 deletions(-) create mode 100644 cmd/yurt-manager/names/controller_names.go delete mode 100644 pkg/yurtmanager/controller/servicetopology/common.go delete mode 100644 pkg/yurtmanager/controller/util/controller_utils.go delete mode 100644 pkg/yurtmanager/controller/util/controller_utils_test.go diff --git a/charts/yurt-manager/values.yaml b/charts/yurt-manager/values.yaml index f6d9eba5fc2..30033aa508a 100644 --- a/charts/yurt-manager/values.yaml +++ b/charts/yurt-manager/values.yaml @@ -21,7 +21,7 @@ ports: webhook: 10273 # format should be "foo,-bar,*" -controllers: "" +controllers: "*" # format should be "foo,*" disableIndependentWebhooks: "" diff --git a/cmd/yurt-manager/app/manager.go b/cmd/yurt-manager/app/manager.go index 05e2a2c40bd..9c11a76449a 100644 --- a/cmd/yurt-manager/app/manager.go +++ b/cmd/yurt-manager/app/manager.go @@ -36,6 +36,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/options" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/apis" "github.com/openyurtio/openyurt/pkg/projectinfo" "github.com/openyurtio/openyurt/pkg/util/profile" @@ -93,7 +94,7 @@ current state towards the desired state.`, PrintFlags(cmd.Flags()) - c, err := s.Config() + c, err := s.Config(controller.KnownControllers(), names.YurtManagerControllerAliases()) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) @@ -115,7 +116,7 @@ current state towards the desired state.`, } fs := cmd.Flags() - namedFlagSets := s.Flags() + namedFlagSets := s.Flags(controller.KnownControllers(), controller.ControllersDisabledByDefault.List()) // verflag.AddFlags(namedFlagSets.FlagSet("global")) globalflag.AddGlobalFlags(namedFlagSets.FlagSet("global"), cmd.Name()) for _, f := range namedFlagSets.FlagSets { diff --git a/cmd/yurt-manager/app/options/generic.go b/cmd/yurt-manager/app/options/generic.go index 919fd504d04..783e74dc3ea 100644 --- a/cmd/yurt-manager/app/options/generic.go +++ b/cmd/yurt-manager/app/options/generic.go @@ -18,15 +18,15 @@ package options import ( "fmt" + "strings" "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/util/sets" "github.com/openyurtio/openyurt/pkg/features" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/apis/config" ) -const enableAll = "*" - type GenericOptions struct { *config.GenericConfiguration } @@ -43,14 +43,13 @@ func NewGenericOptions() *GenericOptions { RestConfigQPS: 30, RestConfigBurst: 50, WorkingNamespace: "kube-system", - Controllers: []string{enableAll}, DisabledWebhooks: []string{}, }, } } // Validate checks validation of GenericOptions. -func (o *GenericOptions) Validate() []error { +func (o *GenericOptions) Validate(allControllers []string, controllerAliases map[string]string) []error { if o == nil { return nil } @@ -59,11 +58,26 @@ func (o *GenericOptions) Validate() []error { if o.WebhookPort == 0 { errs = append(errs, fmt.Errorf("webhook server can not be switched off with 0")) } + + allControllersSet := sets.NewString(allControllers...) + for _, initialName := range o.Controllers { + if initialName == "*" { + continue + } + initialNameWithoutPrefix := strings.TrimPrefix(initialName, "-") + controllerName := initialNameWithoutPrefix + if canonicalName, ok := controllerAliases[controllerName]; ok { + controllerName = canonicalName + } + if !allControllersSet.Has(controllerName) { + errs = append(errs, fmt.Errorf("%q is not in the list of known controllers", initialNameWithoutPrefix)) + } + } return errs } // ApplyTo fills up generic config with options. -func (o *GenericOptions) ApplyTo(cfg *config.GenericConfiguration) error { +func (o *GenericOptions) ApplyTo(cfg *config.GenericConfiguration, controllerAliases map[string]string) error { if o == nil { return nil } @@ -77,14 +91,26 @@ func (o *GenericOptions) ApplyTo(cfg *config.GenericConfiguration) error { cfg.RestConfigQPS = o.RestConfigQPS cfg.RestConfigBurst = o.RestConfigBurst cfg.WorkingNamespace = o.WorkingNamespace - cfg.Controllers = o.Controllers + + cfg.Controllers = make([]string, len(o.Controllers)) + for i, initialName := range o.Controllers { + initialNameWithoutPrefix := strings.TrimPrefix(initialName, "-") + controllerName := initialNameWithoutPrefix + if canonicalName, ok := controllerAliases[controllerName]; ok { + controllerName = canonicalName + } + if strings.HasPrefix(initialName, "-") { + controllerName = fmt.Sprintf("-%s", controllerName) + } + cfg.Controllers[i] = controllerName + } cfg.DisabledWebhooks = o.DisabledWebhooks return nil } // AddFlags adds flags related to generic for yurt-manager to the specified FlagSet. -func (o *GenericOptions) AddFlags(fs *pflag.FlagSet) { +func (o *GenericOptions) AddFlags(fs *pflag.FlagSet, allControllers, disabledByDefaultControllers []string) { if o == nil { return } @@ -94,14 +120,14 @@ func (o *GenericOptions) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&o.HealthProbeAddr, "health-probe-addr", o.HealthProbeAddr, "The address the healthz/readyz endpoint binds to.") fs.IntVar(&o.WebhookPort, "webhook-port", o.WebhookPort, "The port on which to serve HTTPS for webhook server. It can't be switched off with 0") fs.BoolVar(&o.EnableLeaderElection, "enable-leader-election", o.EnableLeaderElection, "Whether you need to enable leader election.") - fs.IntVar(&o.RestConfigQPS, "rest-config-qps", o.RestConfigQPS, "rest-config-qps.") fs.IntVar(&o.RestConfigBurst, "rest-config-burst", o.RestConfigBurst, "rest-config-burst.") fs.StringVar(&o.WorkingNamespace, "working-namespace", o.WorkingNamespace, "The namespace where the yurt-manager is working.") - fs.StringSliceVar(&o.Controllers, "controllers", o.Controllers, "A list of controllers to enable. "+ - "'*' enables all on-by-default controllers, 'foo' enables the controller named 'foo', '-foo' disables the controller named 'foo'.") + fs.StringSliceVar(&o.Controllers, "controllers", o.Controllers, fmt.Sprintf("A list of controllers to enable. '*' enables all on-by-default controllers, 'foo' enables the controller "+ + "named 'foo', '-foo' disables the controller named 'foo'.\nAll controllers: %s\nDisabled-by-default controllers: %s", + strings.Join(allControllers, ", "), strings.Join(disabledByDefaultControllers, ", "))) fs.StringSliceVar(&o.DisabledWebhooks, "disable-independent-webhooks", o.DisabledWebhooks, "A list of webhooks to disable. "+ - "'*' disables all webhooks, 'foo' disables the webhook named 'foo'.") + "'*' disables all independent webhooks, 'foo' disables the independent webhook named 'foo'.") features.DefaultMutableFeatureGate.AddFlag(fs) } diff --git a/cmd/yurt-manager/app/options/options.go b/cmd/yurt-manager/app/options/options.go index 1d1237b46b1..e0940d30971 100644 --- a/cmd/yurt-manager/app/options/options.go +++ b/cmd/yurt-manager/app/options/options.go @@ -50,9 +50,9 @@ func NewYurtManagerOptions() (*YurtManagerOptions, error) { return &s, nil } -func (y *YurtManagerOptions) Flags() cliflag.NamedFlagSets { +func (y *YurtManagerOptions) Flags(allControllers, disabledByDefaultControllers []string) cliflag.NamedFlagSets { fss := cliflag.NamedFlagSets{} - y.Generic.AddFlags(fss.FlagSet("generic")) + y.Generic.AddFlags(fss.FlagSet("generic"), allControllers, disabledByDefaultControllers) y.NodePoolController.AddFlags(fss.FlagSet("nodepool controller")) y.GatewayPickupController.AddFlags(fss.FlagSet("gateway controller")) y.YurtStaticSetController.AddFlags(fss.FlagSet("yurtstaticset controller")) @@ -64,9 +64,9 @@ func (y *YurtManagerOptions) Flags() cliflag.NamedFlagSets { } // Validate is used to validate the options and config before launching the yurt-manager -func (y *YurtManagerOptions) Validate() error { +func (y *YurtManagerOptions) Validate(allControllers []string, controllerAliases map[string]string) error { var errs []error - errs = append(errs, y.Generic.Validate()...) + errs = append(errs, y.Generic.Validate(allControllers, controllerAliases)...) errs = append(errs, y.NodePoolController.Validate()...) errs = append(errs, y.GatewayPickupController.Validate()...) errs = append(errs, y.YurtStaticSetController.Validate()...) @@ -76,8 +76,8 @@ func (y *YurtManagerOptions) Validate() error { } // ApplyTo fills up yurt manager config with options. -func (y *YurtManagerOptions) ApplyTo(c *config.Config) error { - if err := y.Generic.ApplyTo(&c.ComponentConfig.Generic); err != nil { +func (y *YurtManagerOptions) ApplyTo(c *config.Config, controllerAliases map[string]string) error { + if err := y.Generic.ApplyTo(&c.ComponentConfig.Generic, controllerAliases); err != nil { return err } if err := y.NodePoolController.ApplyTo(&c.ComponentConfig.NodePoolController); err != nil { @@ -100,13 +100,13 @@ func (y *YurtManagerOptions) ApplyTo(c *config.Config) error { } // Config return a yurt-manager config objective -func (y *YurtManagerOptions) Config() (*config.Config, error) { - if err := y.Validate(); err != nil { +func (y *YurtManagerOptions) Config(allControllers []string, controllerAliases map[string]string) (*config.Config, error) { + if err := y.Validate(allControllers, controllerAliases); err != nil { return nil, err } c := &config.Config{} - if err := y.ApplyTo(c); err != nil { + if err := y.ApplyTo(c, controllerAliases); err != nil { return nil, err } diff --git a/cmd/yurt-manager/names/controller_names.go b/cmd/yurt-manager/names/controller_names.go new file mode 100644 index 00000000000..f00dc8d04d0 --- /dev/null +++ b/cmd/yurt-manager/names/controller_names.go @@ -0,0 +1,58 @@ +/* +Copyright 2023 The OpenYurt Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package names + +const ( + CsrApproverController = "csr-approver-controller" + DaemonPodUpdaterController = "daemon-pod-updater-controller" + NodePoolController = "nodepool-controller" + PlatformAdminController = "platform-admin-controller" + ServiceTopologyEndpointsController = "service-topology-endpoints-controller" + ServiceTopologyEndpointSliceController = "service-topology-endpointslice-controller" + YurtAppSetController = "yurt-app-set-controller" + YurtAppDaemonController = "yurt-app-daemon-controller" + YurtStaticSetController = "yurt-static-set-controller" + YurtCoordinatorCertController = "yurt-coordinator-cert-controller" + DelegateLeaseController = "delegate-lease-controller" + PodBindingController = "pod-binding-controller" + GatewayPickupController = "gateway-pickup-controller" + GatewayInternalServiceController = "gateway-internal-service-controller" + GatewayPublicServiceController = "gateway-public-service" + GatewayDNSController = "gateway-dns-controller" +) + +func YurtManagerControllerAliases() map[string]string { + // return a new reference to achieve immutability of the mapping + return map[string]string{ + "csrapprover": CsrApproverController, + "daemonpodupdater": DaemonPodUpdaterController, + "nodepool": NodePoolController, + "platformadmin": PlatformAdminController, + "servicetopologyendpoints": ServiceTopologyEndpointsController, + "servicetopologyendpointslices": ServiceTopologyEndpointSliceController, + "yurtappset": YurtAppSetController, + "yurtappdaemon": YurtAppDaemonController, + "yurtstaticset": YurtStaticSetController, + "yurtcoordinatorcert": YurtCoordinatorCertController, + "delegatelease": DelegateLeaseController, + "podbinding": PodBindingController, + "gatewaypickup": GatewayPickupController, + "gatewayinternalservice": GatewayInternalServiceController, + "gatewaypublicservice": GatewayPublicServiceController, + "gatewaydns": GatewayDNSController, + } +} diff --git a/go.mod b/go.mod index 6bdad04c789..3c16f9c42eb 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( k8s.io/cluster-bootstrap v0.22.3 k8s.io/component-base v0.22.3 k8s.io/component-helpers v0.22.3 + k8s.io/controller-manager v0.22.3 k8s.io/klog/v2 v2.9.0 k8s.io/kubectl v0.22.3 k8s.io/kubernetes v1.22.3 diff --git a/go.sum b/go.sum index e3fa03ef033..1a6aba74e96 100644 --- a/go.sum +++ b/go.sum @@ -1173,6 +1173,7 @@ k8s.io/component-base v0.22.3 h1:/+hryAW03u3FpJQww+GSMsArJNUbGjH66lrgxaRynLU= k8s.io/component-base v0.22.3/go.mod h1:kuybv1miLCMoOk3ebrqF93GbQHQx6W2287FC0YEQY6s= k8s.io/component-helpers v0.22.3 h1:08tn+T8HnjRTwDP2ErIBhHGvPcYJf5zWaWW83golHWc= k8s.io/component-helpers v0.22.3/go.mod h1:7OVySVH5elhHKuJKUOxZEfpT1Bm3ChmBQZHmuFfbGHk= +k8s.io/controller-manager v0.22.3 h1:nBKG8MsgtUd/oFaZvE5zAYRIr45+Hn8QkHzq5+CtPOE= k8s.io/controller-manager v0.22.3/go.mod h1:4cvQGMvYf6IpTY08/NigEiI5UrN/cbtOe5e5WepYmcQ= k8s.io/cri-api v0.22.3/go.mod h1:mj5DGUtElRyErU5AZ8EM0ahxbElYsaLAMTPhLPQ40Eg= k8s.io/csi-translation-lib v0.22.3/go.mod h1:YkdI+scWhZJQeA26iNg9XrKO3LhLz6dAcRKsL0RIiUY= diff --git a/pkg/yurtmanager/controller/controller.go b/pkg/yurtmanager/controller/controller.go index 06f16c73b13..d30b7d8e2dd 100644 --- a/pkg/yurtmanager/controller/controller.go +++ b/pkg/yurtmanager/controller/controller.go @@ -17,24 +17,26 @@ limitations under the License. package controller import ( + "fmt" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/controller-manager/app" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/csrapprover" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/daemonpodupdater" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/dns" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewayinternalservice" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypickup" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/gatewaypublicservice" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" servicetopologyendpoints "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/endpoints" servicetopologyendpointslice "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/endpointslice" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset" yurtcoordinatorcert "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/cert" @@ -43,33 +45,51 @@ import ( "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset" ) -// Note !!! @kadisi -// Do not change the name of the file @kadisi -// Note !!! - -// Don`t Change this Name !!!! @kadisi -// TODO support feature gate @kadisi -type AddControllerFn func(*config.CompletedConfig, manager.Manager) error - -var controllerAddFuncs = make(map[string][]AddControllerFn) - -func init() { - controllerAddFuncs[csrapprover.ControllerName] = []AddControllerFn{csrapprover.Add} - controllerAddFuncs[daemonpodupdater.ControllerName] = []AddControllerFn{daemonpodupdater.Add} - controllerAddFuncs[delegatelease.ControllerName] = []AddControllerFn{delegatelease.Add} - controllerAddFuncs[podbinding.ControllerName] = []AddControllerFn{podbinding.Add} - controllerAddFuncs[nodepool.ControllerName] = []AddControllerFn{nodepool.Add} - controllerAddFuncs[yurtcoordinatorcert.ControllerName] = []AddControllerFn{yurtcoordinatorcert.Add} - controllerAddFuncs[servicetopology.ControllerName] = []AddControllerFn{servicetopologyendpoints.Add, servicetopologyendpointslice.Add} - controllerAddFuncs[yurtstaticset.ControllerName] = []AddControllerFn{yurtstaticset.Add} - controllerAddFuncs[yurtappset.ControllerName] = []AddControllerFn{yurtappset.Add} - controllerAddFuncs[yurtappdaemon.ControllerName] = []AddControllerFn{yurtappdaemon.Add} - controllerAddFuncs[platformadmin.ControllerName] = []AddControllerFn{platformadmin.Add} - - controllerAddFuncs[raven.GatewayPickupControllerName] = []AddControllerFn{gatewaypickup.Add} - controllerAddFuncs[raven.GatewayDNSControllerName] = []AddControllerFn{dns.Add} - controllerAddFuncs[raven.GatewayInternalServiceController] = []AddControllerFn{gatewayinternalservice.Add} - controllerAddFuncs[raven.GatewayPublicServiceController] = []AddControllerFn{gatewaypublicservice.Add} +type InitFunc func(*config.CompletedConfig, manager.Manager) error + +type ControllerInitializersFunc func() (initializers map[string]InitFunc) + +var ( + _ ControllerInitializersFunc = NewControllerInitializers + + // ControllersDisabledByDefault is the set of controllers which is disabled by default + ControllersDisabledByDefault = sets.NewString() +) + +// KnownControllers returns all known controllers's name +func KnownControllers() []string { + ret := sets.StringKeySet(NewControllerInitializers()) + + return ret.List() +} + +func NewControllerInitializers() map[string]InitFunc { + controllers := map[string]InitFunc{} + register := func(name string, fn InitFunc) { + if _, found := controllers[name]; found { + panic(fmt.Sprintf("controller name %q was registered twice", name)) + } + controllers[name] = fn + } + + register(names.CsrApproverController, csrapprover.Add) + register(names.DaemonPodUpdaterController, daemonpodupdater.Add) + register(names.DelegateLeaseController, delegatelease.Add) + register(names.PodBindingController, podbinding.Add) + register(names.NodePoolController, nodepool.Add) + register(names.YurtCoordinatorCertController, yurtcoordinatorcert.Add) + register(names.ServiceTopologyEndpointsController, servicetopologyendpoints.Add) + register(names.ServiceTopologyEndpointSliceController, servicetopologyendpointslice.Add) + register(names.YurtStaticSetController, yurtstaticset.Add) + register(names.YurtAppSetController, yurtappset.Add) + register(names.YurtAppDaemonController, yurtappdaemon.Add) + register(names.PlatformAdminController, platformadmin.Add) + register(names.GatewayPickupController, gatewaypickup.Add) + register(names.GatewayDNSController, dns.Add) + register(names.GatewayInternalServiceController, gatewayinternalservice.Add) + register(names.GatewayPublicServiceController, gatewaypublicservice.Add) + + return controllers } // If you want to add additional RBAC, enter it here !!! @kadisi @@ -78,22 +98,20 @@ func init() { // +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete func SetupWithManager(c *config.CompletedConfig, m manager.Manager) error { - klog.InfoS("SetupWithManager", "len", len(controllerAddFuncs)) - for controllerName, fns := range controllerAddFuncs { - if !util.IsControllerEnabled(controllerName, c.ComponentConfig.Generic.Controllers) { + for controllerName, fn := range NewControllerInitializers() { + if !app.IsControllerEnabled(controllerName, ControllersDisabledByDefault, c.ComponentConfig.Generic.Controllers) { klog.Warningf("Controller %v is disabled", controllerName) continue } - for _, f := range fns { - if err := f(c, m); err != nil { - if kindMatchErr, ok := err.(*meta.NoKindMatchError); ok { - klog.Infof("CRD %v is not installed, its controller will perform noops!", kindMatchErr.GroupKind) - continue - } - return err + if err := fn(c, m); err != nil { + if kindMatchErr, ok := err.(*meta.NoKindMatchError); ok { + klog.Infof("CRD %v is not installed, its controller will perform noops!", kindMatchErr.GroupKind) + continue } + return err } } + return nil } diff --git a/pkg/yurtmanager/controller/csrapprover/csrapprover_controller.go b/pkg/yurtmanager/controller/csrapprover/csrapprover_controller.go index 0316fb3d66a..ab70a65bef2 100644 --- a/pkg/yurtmanager/controller/csrapprover/csrapprover_controller.go +++ b/pkg/yurtmanager/controller/csrapprover/csrapprover_controller.go @@ -43,6 +43,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/projectinfo" "github.com/openyurtio/openyurt/pkg/yurthub/certificate/token" yurtcoorrdinatorCert "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/cert" @@ -93,10 +94,6 @@ var ( } ) -const ( - ControllerName = "csrapprover" -) - type csrRecognizer struct { recognize func(csr *certificatesv1.CertificateSigningRequest, x509cr *x509.CertificateRequest) bool successMsg string @@ -107,7 +104,7 @@ type csrRecognizer struct { func Add(_ *appconfig.CompletedConfig, mgr manager.Manager) error { r := &ReconcileCsrApprover{} // Create a new controller - c, err := controller.New(ControllerName, mgr, controller.Options{ + c, err := controller.New(names.CsrApproverController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller.go b/pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller.go index 726acefc586..bc6691813a4 100644 --- a/pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller.go +++ b/pkg/yurtmanager/controller/daemonpodupdater/daemon_pod_updater_controller.go @@ -49,6 +49,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" k8sutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/daemonpodupdater/kubernetes" ) @@ -64,8 +65,6 @@ var ( ) const ( - ControllerName = "daemonpodupdater" - // UpdateAnnotation is the annotation key used in daemonset spec to indicate // which update strategy is selected. Currently, "OTA" and "AdvancedRollingUpdate" are supported. UpdateAnnotation = "apps.openyurt.io/update-strategy" @@ -95,7 +94,7 @@ const ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) + return fmt.Sprintf("%s: %s", names.DaemonPodUpdaterController, s) } // Add creates a new Daemonpodupdater Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller @@ -120,7 +119,7 @@ func newReconciler(_ *appconfig.CompletedConfig, mgr manager.Manager) reconcile. return &ReconcileDaemonpodupdater{ Client: mgr.GetClient(), expectations: k8sutil.NewControllerExpectations(), - recorder: mgr.GetEventRecorderFor(ControllerName), + recorder: mgr.GetEventRecorderFor(names.DaemonPodUpdaterController), } } @@ -141,7 +140,7 @@ func (r *ReconcileDaemonpodupdater) InjectConfig(cfg *rest.Config) error { // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) + c, err := controller.New(names.DaemonPodUpdaterController, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) if err != nil { return err } diff --git a/pkg/yurtmanager/controller/nodepool/nodepool_controller.go b/pkg/yurtmanager/controller/nodepool/nodepool_controller.go index e7372938c10..913fb2e9ba3 100644 --- a/pkg/yurtmanager/controller/nodepool/nodepool_controller.go +++ b/pkg/yurtmanager/controller/nodepool/nodepool_controller.go @@ -33,6 +33,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1beta1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1beta1" poolconfig "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool/config" @@ -43,13 +44,9 @@ var ( controllerResource = appsv1beta1.SchemeGroupVersion.WithResource("nodepools") ) -const ( - ControllerName = "nodepool" -) - func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) + return fmt.Sprintf("%s: %s", names.NodePoolController, s) } // ReconcileNodePool reconciles a NodePool object @@ -78,11 +75,11 @@ func Add(c *config.CompletedConfig, mgr manager.Manager) error { klog.Infof("nodepool-controller add controller %s", controllerResource.String()) r := &ReconcileNodePool{ cfg: c.ComponentConfig.NodePoolController, - recorder: mgr.GetEventRecorderFor(ControllerName), + recorder: mgr.GetEventRecorderFor(names.NodePoolController), } // Create a new controller - ctrl, err := controller.New(ControllerName, mgr, controller.Options{ + ctrl, err := controller.New(names.NodePoolController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go index 6d01a0ea155..a2e8b932a8d 100644 --- a/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go +++ b/pkg/yurtmanager/controller/platformadmin/platformadmin_controller.go @@ -45,6 +45,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/apis/apps" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" iotv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/iot/v1alpha1" @@ -59,7 +60,7 @@ func init() { func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) + return fmt.Sprintf("%s: %s", names.PlatformAdminController, s) } var ( @@ -68,8 +69,6 @@ var ( ) const ( - ControllerName = "PlatformAdmin" - LabelConfigmap = "Configmap" LabelService = "Service" LabelDeployment = "Deployment" @@ -135,7 +134,7 @@ func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile. return &ReconcilePlatformAdmin{ Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(ControllerName), + recorder: mgr.GetEventRecorderFor(names.PlatformAdminController), yamlSerializer: kjson.NewSerializerWithOptions(kjson.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, kjson.SerializerOptions{Yaml: true, Pretty: true}), Configration: c.ComponentConfig.PlatformAdminController, } @@ -144,7 +143,7 @@ func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile. // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(ControllerName, mgr, controller.Options{ + c, err := controller.New(names.PlatformAdminController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/raven/common.go b/pkg/yurtmanager/controller/raven/common.go index 9b94c11f0fc..b69473e47c5 100644 --- a/pkg/yurtmanager/controller/raven/common.go +++ b/pkg/yurtmanager/controller/raven/common.go @@ -19,10 +19,3 @@ package raven var ( ConcurrentReconciles = 1 ) - -const ( - GatewayPickupControllerName = "raven-gateway-pickup" - GatewayInternalServiceController = "raven-gateway-internal-service" - GatewayPublicServiceController = "raven-gateway-public-service" - GatewayDNSControllerName = "raven-dns" -) diff --git a/pkg/yurtmanager/controller/raven/dns/dns_controller.go b/pkg/yurtmanager/controller/raven/dns/dns_controller.go index 43a3e4435c7..17eb950e7c7 100644 --- a/pkg/yurtmanager/controller/raven/dns/dns_controller.go +++ b/pkg/yurtmanager/controller/raven/dns/dns_controller.go @@ -40,13 +40,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" ) func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", common.GatewayDNSControllerName, s) + return fmt.Sprintf("%s: %s", names.GatewayDNSController, s) } // Add creates a new Ravendns Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller @@ -68,14 +69,14 @@ func newReconciler(mgr manager.Manager) reconcile.Reconciler { return &ReconcileDns{ Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(common.GatewayDNSControllerName), + recorder: mgr.GetEventRecorderFor(names.GatewayDNSController), } } // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(common.GatewayDNSControllerName, mgr, controller.Options{ + c, err := controller.New(names.GatewayDNSController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller.go b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller.go index 0f1e7c8946c..15277e96218 100644 --- a/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller.go +++ b/pkg/yurtmanager/controller/raven/gatewayinternalservice/gateway_internal_service_controller.go @@ -43,6 +43,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven/utils" @@ -55,7 +56,7 @@ const ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", common.GatewayInternalServiceController, s) + return fmt.Sprintf("%s: %s", names.GatewayInternalServiceController, s) } // Add creates a new Service Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller @@ -79,7 +80,7 @@ func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile. return &ReconcileService{ Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(common.GatewayInternalServiceController), + recorder: mgr.GetEventRecorderFor(names.GatewayInternalServiceController), option: utils.NewOption(), } } @@ -87,7 +88,7 @@ func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile. // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(common.GatewayInternalServiceController, mgr, controller.Options{ + c, err := controller.New(names.GatewayInternalServiceController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go index 3f56bb8ea0f..4a6c2455d60 100644 --- a/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go +++ b/pkg/yurtmanager/controller/raven/gatewaypickup/gateway_pickup_controller.go @@ -41,6 +41,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/apis/raven" ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" @@ -55,7 +56,7 @@ var ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", common.GatewayPickupControllerName, s) + return fmt.Sprintf("%s: %s", names.GatewayPickupController, s) } const ( @@ -90,7 +91,7 @@ func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile. return &ReconcileGateway{ Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(common.GatewayPickupControllerName), + recorder: mgr.GetEventRecorderFor(names.GatewayPickupController), Configration: c.ComponentConfig.GatewayPickupController, } } @@ -98,7 +99,7 @@ func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile. // add is used to add a new Controller to mgr func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(common.GatewayPickupControllerName, mgr, controller.Options{ + c, err := controller.New(names.GatewayPickupController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller.go b/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller.go index 0bc1d19ed09..ad43608c58a 100644 --- a/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller.go +++ b/pkg/yurtmanager/controller/raven/gatewaypublicservice/gateway_public_service_controller.go @@ -41,6 +41,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/apis/raven" ravenv1beta1 "github.com/openyurtio/openyurt/pkg/apis/raven/v1beta1" common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" @@ -53,7 +54,7 @@ const ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", common.GatewayPublicServiceController, s) + return fmt.Sprintf("%s: %s", names.GatewayPublicServiceController, s) } // Add creates a new Service Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller @@ -104,7 +105,7 @@ func newReconciler(mgr manager.Manager) reconcile.Reconciler { return &ReconcileService{ Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(common.GatewayPublicServiceController), + recorder: mgr.GetEventRecorderFor(names.GatewayPublicServiceController), option: utils.NewOption(), svcInfo: newServiceInfo(), } @@ -113,7 +114,7 @@ func newReconciler(mgr manager.Manager) reconcile.Reconciler { // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(common.GatewayPublicServiceController, mgr, controller.Options{ + c, err := controller.New(names.GatewayPublicServiceController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: common.ConcurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/servicetopology/common.go b/pkg/yurtmanager/controller/servicetopology/common.go deleted file mode 100644 index fa58c1c01ef..00000000000 --- a/pkg/yurtmanager/controller/servicetopology/common.go +++ /dev/null @@ -1,21 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the License); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an AS IS BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package servicetopology - -const ( - ControllerName = "servicetopology" -) diff --git a/pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_controller.go b/pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_controller.go index 39adacb3088..bb0d3407016 100644 --- a/pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_controller.go +++ b/pkg/yurtmanager/controller/servicetopology/endpoints/endpoints_controller.go @@ -31,7 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/adapter" ) @@ -46,7 +46,7 @@ var ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s-endpoints: %s", common.ControllerName, s) + return fmt.Sprintf("%s: %s", names.ServiceTopologyEndpointsController, s) } // Add creates a new Servicetopology endpoints Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller @@ -78,7 +78,7 @@ func (r *ReconcileServicetopologyEndpoints) InjectClient(c client.Client) error // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(fmt.Sprintf("%s-endpoints", common.ControllerName), mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) + c, err := controller.New(names.ServiceTopologyEndpointsController, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) if err != nil { return err } diff --git a/pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_controller.go b/pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_controller.go index 8a26925f55f..f5541322571 100644 --- a/pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_controller.go +++ b/pkg/yurtmanager/controller/servicetopology/endpointslice/endpointslice_controller.go @@ -34,7 +34,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - common "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/servicetopology/adapter" ) @@ -49,14 +49,14 @@ var ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s-endpointslice: %s", common.ControllerName, s) + return fmt.Sprintf("%s: %s", names.ServiceTopologyEndpointSliceController, s) } // Add creates a new Servicetopology endpointslice Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(_ *appconfig.CompletedConfig, mgr manager.Manager) error { r := &ReconcileServiceTopologyEndpointSlice{} - c, err := controller.New(fmt.Sprintf("%s-endpointslice", common.ControllerName), mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) + c, err := controller.New(names.ServiceTopologyEndpointSliceController, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) if err != nil { return err } @@ -74,7 +74,7 @@ func Add(_ *appconfig.CompletedConfig, mgr manager.Manager) error { return err } - klog.Infof("%s-endpointslice controller is added", common.ControllerName) + klog.Infof("%s controller is added", names.ServiceTopologyEndpointSliceController) return nil } diff --git a/pkg/yurtmanager/controller/util/controller_utils.go b/pkg/yurtmanager/controller/util/controller_utils.go deleted file mode 100644 index 09879cbb825..00000000000 --- a/pkg/yurtmanager/controller/util/controller_utils.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -// IsControllerEnabled check if a specified controller enabled or not. -func IsControllerEnabled(name string, controllers []string) bool { - for _, ctrl := range controllers { - if ctrl == name { - return true - } - if ctrl == "-"+name { - return false - } - if ctrl == "*" { - return true - } - } - - return false -} diff --git a/pkg/yurtmanager/controller/util/controller_utils_test.go b/pkg/yurtmanager/controller/util/controller_utils_test.go deleted file mode 100644 index 6eb75008e35..00000000000 --- a/pkg/yurtmanager/controller/util/controller_utils_test.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2023 The OpenYurt Authors. - -Licensed under the Apache License, Version 2.0 (the License); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an AS IS BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package util - -import "testing" - -func TestIsControllerEnabled(t *testing.T) { - testcases := map[string]struct { - name string - controllers []string - result bool - }{ - "enable specified controller": { - name: "foo", - controllers: []string{"foo", "bar"}, - result: true, - }, - "disable specified controller": { - name: "foo", - controllers: []string{"-foo", "bar"}, - result: false, - }, - "enable controller in default": { - name: "foo", - controllers: []string{"bar", "*"}, - result: true, - }, - "controller doesn't exist": { - name: "unknown", - controllers: []string{"foo", "bar"}, - result: false, - }, - } - - for k, tc := range testcases { - t.Run(k, func(t *testing.T) { - result := IsControllerEnabled(tc.name, tc.controllers) - if tc.result != result { - t.Errorf("expect controller enabled: %v, but got %v", tc.result, result) - } - }) - } -} diff --git a/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller.go b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller.go index 097a092ef07..05df1435db7 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller.go @@ -38,6 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller" @@ -49,7 +50,6 @@ var ( ) const ( - ControllerName = "yurtappdaemon" slowStartInitialBatchSize = 1 eventTypeRevisionProvision = "RevisionProvision" @@ -66,7 +66,7 @@ func init() { func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) + return fmt.Sprintf("%s: %s", names.YurtAppDaemonController, s) } // Add creates a new YurtAppDaemon Controller and adds it to the Manager with default RBAC. @@ -85,7 +85,7 @@ func Add(c *config.CompletedConfig, mgr manager.Manager) error { // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) + c, err := controller.New(names.YurtAppDaemonController, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) if err != nil { return err } @@ -118,10 +118,9 @@ type ReconcileYurtAppDaemon struct { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { return &ReconcileYurtAppDaemon{ - Client: mgr.GetClient(), - scheme: mgr.GetScheme(), - - recorder: mgr.GetEventRecorderFor(ControllerName), + Client: mgr.GetClient(), + scheme: mgr.GetScheme(), + recorder: mgr.GetEventRecorderFor(names.YurtAppDaemonController), controls: map[unitv1alpha1.TemplateType]workloadcontroller.WorkloadController{ // unitv1alpha1.StatefulSetTemplateType: &StatefulSetControllor{Client: mgr.GetClient(), scheme: mgr.GetScheme()}, unitv1alpha1.DeploymentTemplateType: &workloadcontroller.DeploymentControllor{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}, diff --git a/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go index bfdf9091125..5611e911552 100644 --- a/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go +++ b/pkg/yurtmanager/controller/yurtappdaemon/yurtappdaemon_controller_test.go @@ -25,6 +25,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/apis/apps" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon/workloadcontroller" @@ -125,7 +126,7 @@ func TestUpdateStatus(t *testing.T) { "equal", yad, &unitv1alpha1.YurtAppDaemonStatus{ - CurrentRevision: ControllerName, + CurrentRevision: names.YurtAppDaemonController, CollisionCount: &int1, TemplateType: "StatefulSet", ObservedGeneration: 1, @@ -139,7 +140,7 @@ func TestUpdateStatus(t *testing.T) { }, }, &unitv1alpha1.YurtAppDaemonStatus{ - CurrentRevision: ControllerName, + CurrentRevision: names.YurtAppDaemonController, CollisionCount: &int1, TemplateType: "StatefulSet", ObservedGeneration: 1, @@ -196,7 +197,7 @@ func TestUpdateYurtAppDaemon(t *testing.T) { "equal", yad, &unitv1alpha1.YurtAppDaemonStatus{ - CurrentRevision: ControllerName, + CurrentRevision: names.YurtAppDaemonController, CollisionCount: &int1, TemplateType: "StatefulSet", ObservedGeneration: 1, @@ -210,7 +211,7 @@ func TestUpdateYurtAppDaemon(t *testing.T) { }, }, &unitv1alpha1.YurtAppDaemonStatus{ - CurrentRevision: ControllerName, + CurrentRevision: names.YurtAppDaemonController, CollisionCount: &int1, TemplateType: "StatefulSet", ObservedGeneration: 1, diff --git a/pkg/yurtmanager/controller/yurtappset/yurtappset_controller.go b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller.go index 2d3edaa2dd5..750e57a8f05 100644 --- a/pkg/yurtmanager/controller/yurtappset/yurtappset_controller.go +++ b/pkg/yurtmanager/controller/yurtappset/yurtappset_controller.go @@ -41,6 +41,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" unitv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset/adapter" ) @@ -55,8 +56,6 @@ var ( ) const ( - ControllerName = "yurtappset" - eventTypeRevisionProvision = "RevisionProvision" eventTypeFindPools = "FindPools" eventTypeDupPoolsDelete = "DeleteDuplicatedPools" @@ -68,7 +67,7 @@ const ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) + return fmt.Sprintf("%s: %s", names.YurtAppSetController, s) } // Add creates a new YurtAppSet Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller @@ -89,7 +88,7 @@ func newReconciler(c *config.CompletedConfig, mgr manager.Manager) reconcile.Rec Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(ControllerName), + recorder: mgr.GetEventRecorderFor(names.YurtAppSetController), poolControls: map[unitv1alpha1.TemplateType]ControlInterface{ unitv1alpha1.StatefulSetTemplateType: &PoolControl{Client: mgr.GetClient(), scheme: mgr.GetScheme(), adapter: &adapter.StatefulSetAdapter{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}}, @@ -102,7 +101,7 @@ func newReconciler(c *config.CompletedConfig, mgr manager.Manager) reconcile.Rec // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) + c, err := controller.New(names.YurtAppSetController, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) if err != nil { return err } diff --git a/pkg/yurtmanager/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go b/pkg/yurtmanager/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go index 414e96f6839..dc127095225 100644 --- a/pkg/yurtmanager/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go +++ b/pkg/yurtmanager/controller/yurtcoordinator/cert/yurtcoordinatorcert_controller.go @@ -39,6 +39,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" certfactory "github.com/openyurtio/openyurt/pkg/util/certmanager/factory" "github.com/openyurtio/openyurt/pkg/util/ip" ) @@ -53,8 +54,6 @@ var ( ) const ( - ControllerName = "yurtcoordinatorcert" - // tmp file directory for certmanager to write cert files certDir = "/tmp" @@ -199,7 +198,7 @@ var ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) + return fmt.Sprintf("%s: %s", names.YurtCoordinatorCertController, s) } // Add creates a new YurtCoordinatorcert Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller @@ -208,7 +207,7 @@ func Add(cfg *appconfig.CompletedConfig, mgr manager.Manager) error { r := &ReconcileYurtCoordinatorCert{} // Create a new controller - c, err := controller.New(ControllerName, mgr, controller.Options{ + c, err := controller.New(names.YurtCoordinatorCertController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller.go b/pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller.go index 7420f6007a1..d06ff113d71 100644 --- a/pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller.go +++ b/pkg/yurtmanager/controller/yurtcoordinator/delegatelease/delegatelease_controller.go @@ -36,6 +36,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" nodeutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/constant" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/utils" @@ -45,10 +46,6 @@ func init() { flag.IntVar(&concurrentReconciles, "delegatelease-controller", concurrentReconciles, "Max concurrent workers for delegatelease-controller controller.") } -const ( - ControllerName = "delegatelease" -) - var ( concurrentReconciles = 5 ) @@ -67,7 +64,7 @@ func Add(_ *appconfig.CompletedConfig, mgr manager.Manager) error { ldc: utils.NewLeaseDelegatedCounter(), delLdc: utils.NewLeaseDelegatedCounter(), } - c, err := controller.New(ControllerName, mgr, controller.Options{ + c, err := controller.New(names.DelegateLeaseController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller.go b/pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller.go index cdd02666f58..bcea5dbabd7 100644 --- a/pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller.go +++ b/pkg/yurtmanager/controller/yurtcoordinator/podbinding/podbinding_controller.go @@ -33,6 +33,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" "github.com/openyurtio/openyurt/pkg/projectinfo" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtcoordinator/constant" ) @@ -41,10 +42,6 @@ func init() { flag.IntVar(&concurrentReconciles, "podbinding-controller", concurrentReconciles, "Max concurrent workers for podbinding-controller controller.") } -const ( - ControllerName = "podbinding" -) - var ( controllerKind = appsv1.SchemeGroupVersion.WithKind("Node") concurrentReconciles = 5 @@ -65,7 +62,7 @@ var ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) + return fmt.Sprintf("%s: %s", names.PodBindingController, s) } type ReconcilePodBinding struct { @@ -86,7 +83,7 @@ func newReconciler(_ *appconfig.CompletedConfig, mgr manager.Manager) reconcile. // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { - c, err := controller.New(ControllerName, mgr, controller.Options{ + c, err := controller.New(names.PodBindingController, mgr, controller.Options{ Reconciler: r, MaxConcurrentReconciles: concurrentReconciles, }) if err != nil { diff --git a/pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller.go b/pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller.go index 16e76fb9092..01bbff4d14b 100644 --- a/pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller.go +++ b/pkg/yurtmanager/controller/yurtstaticset/yurtstaticset_controller.go @@ -41,6 +41,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" appconfig "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" appsv1alpha1 "github.com/openyurtio/openyurt/pkg/apis/apps/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/config" "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset/upgradeinfo" @@ -58,8 +59,6 @@ var ( ) const ( - ControllerName = "yurtstaticset" - StaticPodHashAnnotation = "openyurt.io/static-pod-hash" hostPathVolumeName = "hostpath" @@ -120,7 +119,7 @@ var ( func Format(format string, args ...interface{}) string { s := fmt.Sprintf(format, args...) - return fmt.Sprintf("%s: %s", ControllerName, s) + return fmt.Sprintf("%s: %s", names.YurtStaticSetController, s) } // Add creates a new YurtStaticSet Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller @@ -150,7 +149,7 @@ func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile. return &ReconcileYurtStaticSet{ Client: mgr.GetClient(), scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(ControllerName), + recorder: mgr.GetEventRecorderFor(names.YurtStaticSetController), Configuration: c.ComponentConfig.YurtStaticSetController, } } @@ -158,7 +157,7 @@ func newReconciler(c *appconfig.CompletedConfig, mgr manager.Manager) reconcile. // add adds a new Controller to mgr with r as the reconcile.Reconciler func add(mgr manager.Manager, r reconcile.Reconciler) error { // Create a new controller - c, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) + c, err := controller.New(names.YurtStaticSetController, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: concurrentReconciles}) if err != nil { return err } diff --git a/pkg/yurtmanager/webhook/server.go b/pkg/yurtmanager/webhook/server.go index 47547c10a26..4ed3ff08e5f 100644 --- a/pkg/yurtmanager/webhook/server.go +++ b/pkg/yurtmanager/webhook/server.go @@ -22,18 +22,14 @@ import ( "time" "k8s.io/client-go/rest" + "k8s.io/controller-manager/app" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/openyurtio/openyurt/cmd/yurt-manager/app/config" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/nodepool" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/platformadmin" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/raven" - ctrlutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappdaemon" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtappset" - "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/yurtstaticset" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" + "github.com/openyurtio/openyurt/pkg/yurtmanager/controller" v1beta1gateway "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/gateway/v1beta1" v1node "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/node/v1" v1beta1nodepool "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/nodepool/v1beta1" @@ -73,13 +69,13 @@ func addControllerWebhook(name string, handler SetupWebhookWithManager) { } func init() { - addControllerWebhook(raven.GatewayPickupControllerName, &v1beta1gateway.GatewayHandler{}) - addControllerWebhook(nodepool.ControllerName, &v1beta1nodepool.NodePoolHandler{}) - addControllerWebhook(yurtstaticset.ControllerName, &v1alpha1yurtstaticset.YurtStaticSetHandler{}) - addControllerWebhook(yurtappset.ControllerName, &v1alpha1yurtappset.YurtAppSetHandler{}) - addControllerWebhook(yurtappdaemon.ControllerName, &v1alpha1yurtappdaemon.YurtAppDaemonHandler{}) - addControllerWebhook(platformadmin.ControllerName, &v1alpha1platformadmin.PlatformAdminHandler{}) - addControllerWebhook(platformadmin.ControllerName, &v1alpha2platformadmin.PlatformAdminHandler{}) + addControllerWebhook(names.GatewayPickupController, &v1beta1gateway.GatewayHandler{}) + addControllerWebhook(names.NodePoolController, &v1beta1nodepool.NodePoolHandler{}) + addControllerWebhook(names.YurtStaticSetController, &v1alpha1yurtstaticset.YurtStaticSetHandler{}) + addControllerWebhook(names.YurtAppSetController, &v1alpha1yurtappset.YurtAppSetHandler{}) + addControllerWebhook(names.YurtAppDaemonController, &v1alpha1yurtappdaemon.YurtAppDaemonHandler{}) + addControllerWebhook(names.PlatformAdminController, &v1alpha1platformadmin.PlatformAdminHandler{}) + addControllerWebhook(names.PlatformAdminController, &v1alpha2platformadmin.PlatformAdminHandler{}) independentWebhooks[v1pod.WebhookName] = &v1pod.PodHandler{} independentWebhooks[v1node.WebhookName] = &v1node.NodeHandler{} @@ -125,7 +121,7 @@ func SetupWithManager(c *config.CompletedConfig, mgr manager.Manager) error { // set up controller webhooks for controllerName, list := range controllerWebhooks { - if !ctrlutil.IsControllerEnabled(controllerName, c.ComponentConfig.Generic.Controllers) { + if !app.IsControllerEnabled(controllerName, controller.ControllersDisabledByDefault, c.ComponentConfig.Generic.Controllers) { klog.Warningf("Webhook for %v is disabled", controllerName) continue } diff --git a/test/e2e/cmd/init/constants/constants.go b/test/e2e/cmd/init/constants/constants.go index 228bc16efad..3e3504ec57b 100644 --- a/test/e2e/cmd/init/constants/constants.go +++ b/test/e2e/cmd/init/constants/constants.go @@ -100,6 +100,7 @@ spec: - --metrics-addr=:10271 - --health-probe-addr=:10272 - --webhook-port=10273 + - --controllers=* - --logtostderr=true - --v=4 command:

    +2O?^-(9?-f+$RsoVXP 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 literal 0 HcmV?d00001 diff --git a/docs/img/support-raven-nat-traversal/2.png b/docs/img/support-raven-nat-traversal/2.png new file mode 100644 index 0000000000000000000000000000000000000000..cd6762feed57cb675514fb19e2b41440e94da734 GIT binary patch literal 1519845 zcmeF41$-387sr3+xp)FWLvZ)tP_(#1ajEb}i%ZdB#obDwP-tmsvEpvU-JRePG{pUE z|8Mt_%SEveB5(MF+npVG^PSn*d80F0HgDiyo5L1>L!*XuTLX$bfb5QqnrtGTm#jfQ z>TXRM)P*4a`{Ur6MP#EL)Ue}Fz;&4TCwtH{_eiqT3~SV^p5~d>*15d0;ev`efIMha zw|1M6GR&GWY`Bb^uwepBAbAO_v$wZzCzs2UH&hOd2`~XBzyz286JP>qn1Ea+Lts!4 zlxa8(EIJcNwFJ(dIfH!p@}*k&STH8Q1egF5U;<2l38XXujg1YQot;?$NonvLU&bIH zYWM#B{uv{&oJuB;rU=;WDE&wDT!1tsI19@Jm;e(N zfC(@GCcp%k024^v1Xuw{-TA}PF##sP1egF5U;<2l2{3_lMSvBMl+VrNfC(@GCcp$z839&CQdx$uI81;EFaajO1egF5U;<1aJriIBBt7RYr;Q0P z0Vco%m_TYF(7Dsh^*k3Kwd4lN!30tb0p6-N)no+=!UUKA6JP>Nz>0uO4vmdHv>F9y zdwm!Tkq8S3g~3Dzh-tKNw9`Ts8G?XNJ%nUC`AdM>E)(oEbP$;kBa6WhL2-w{n0yh% z1kP7XfC*$c0z4X$;j)|)%mkPK6JP?VpMVT9y+771YK)@Ab0SC19B9|!C)^ElgjQ>d zr&|`oGk*@`9)BFcT4l0rbtzYAv9-S&T(f$>!^8ZS(>*6Tb)AlfIv1$rR$58whsY8# z0Va@239tf^N;8JV`V<6eO|xfM5v2#vf{;&$y%Tein+bun6qm zw*Xy6tVOt^9^Q8jAt4B% zbC9-enSo{vdg4rkJ(S5d^785UamY-738ZoYvt|q%&KgN7&mF3w{f}h>(QL1ixRPeBaz3CD5Z`*;3 zPiZxYpg}fLQtH$z3CARpMvFpBMi`6&YzpQ;esX(L=7ub8T3ReZ;jl?71XgPX7vZ@8 zX(hKZsGO{fWYCP~WHSLKzyz2;DkUHopvcw$HFJ)|-DfB8)1dY^Te}Imj~R&)4t|iS z?C|EyN(>vc3&9=!Lf3*GSUG}KXR!l+T$^|g;kPc27Ib;eo zY*eIhsAwClEQPUP%^zr1J`0Ut8+5W9^y~$TDmfydU1j?ep8w zvYHY%=S;!M2l^xyc!c5c)f3pgZ95L#dJ5>hu=BTBIOpR4ijoZzU;<3wqX=*{@=^HM zGZSC}ZzB*OlpoG>0p3Q5pEH4^2*{ARL}#o&c>!Cde+_3l8GK)QW6cl4a3IJQDyu;Z znOp^D*DSCjB?VBzF-vx2&z2P~`lmR2^cDii_WG>GaL=0`MannBmLo67<_<1-nV?9h zMIerv$a16G!b5l!9E^agJ29qABp#kRiC-42#T!~*Vii3(X2S%S026pW0tr<;?-v5Q zW&%ur2`~XBzyy+)fLsA3?Ts6uc0=_(zv0%A32=r0|B!Pycg2?$n z7*B%(p(n{QTQB}<@03GuHuiWF#w>ctf>Cc4Z+fXQT_h45hr&t4sUhIXP*{Nd~W z5+R|9#htteI5Z}}1kx`7UJsIf^OjS_1egF5U;=58fQ%G_M@Mn+u?sCC_k^t~3_e;L z&=O;ur(*=|?CfZfdGz9rs07lzNjgpNH9~dvaI}{pQke-wTrxw2M=1F9D(wBSEF8tJ ze%LMSKj@1Bxs{3U=SvGqR2o~@J32w-?~SA9u8~JI?W|=3m5Sm{RB?%thkuv=6Htzy z$a4YSr5La>@-CFv4HIAjOn?b6fs`VkP(gQo1-cBlj5-aW#08 zsqyBjwK77xTG?vhDpQk-FsvQf6%PwIqG7)&Xi%>#rqWuG(=*zj$*IzCv(+Ke_X>7- zRl~kBvrt;2jIURS11hvQ{lKy@=(^7ZijY^hbcB=<8+SBl(FAS^1FbRr1 zc~(M%k=AAih_ow)-aq!l_10r>{OmS7y;L2|d%GZa>mTv2Z7$58{3mwo+bl6rWUq@s zKlMc+Rea@x4sC}%Tyz$OhnEi1pX3Eainqq!i>IMRAxApa-AFOAVFFBm34AyKUQ_nr z?ARw0NQ}U`Q>RX~%ac2IV$a#038Vyp0nNOrM?4Q2)vetoCF0{aJ|O`aO^=dETj>g< zBG3Q@4MA9DWYIIZ_>AmKF_w_aV?ObMiQG%_k%~d~;))LQJ&m4_AB(p*X^_#n6uB(! zb8)Q*s~~Yu@huZz0`Ez{#l;0#vSy`iG}tf!Ccp%k025#Wsg8hnKwx4@f?4s1D{^8= z0x1)uJiO}(AL4<9@9H+L1boi~m;e)aCjzr(3>(h-T)Y!1cEkjj025#WOn?b60Vco% zm_Tw6_*4qWNOxld&jm=19O5vT025#WOn?b60Vco%m_Rxu@TruMbc&0U#RQlD6JP>N zfC(@GCcp%kz{e0^1>|Gsv0o;@1egF5U;@?zCLC`;yGrrC0M<#I_EfZh@On?b60Vco%m;e*_tOQsA`KE+% zAsH;wIk}&PK&F$G^gI{f)3D`GnE(@D0!)AjFoE<-fR&N-o41@YCcp%k025#WOn?b6 z0Va^@2(SW@>av6dVggKn2`~XBzyz2;S|ITIh<%lLE}C(8$esQSp&CdnRq`~YPN8GKJ3 z!Qd_n@E~g|{C4VpC`ry@OBdf?Y#SN1?G-rOcL+xBcmYA3+k#M0VJKY_i#LoxMU4)E)*dH^Ho>re<)}FI7yQ;OH$o!JDbR&H#{9bdu+v|Z zxGd&e8jor&O2||u-1>J3`iwgQUF^if+!gMI{iiy>AxH<_^*aU&UoinDkUj{o0+K%R zkyF70GAe;xE~}_@mj{jN)^3wg8GKl}WpZfjY{{<*wZ3nRGH$leJ-Uvaf1bo&-!DSe zjT6wy(^c%OPcdUM8O);x;W{``tceT`BrT831f#)xpn*C<4{b6hKpZg&3=9Gd8X?p< z6I`!d#I9{mQL$?d5?Fwelp!eXV5^qLjD!$t-R&bd;ID+6))9f>m$2hRAZoS944pxS z>{S|K_)i59Vp2hOeLsHr_a0n|ltJ^BRpF}EL7t-wG~vW1>6G(RCcp%kK$;}L%1D~b zHWr!*Faaj;p#;PxtqaPxX@YizU4Vefa148j=^Gy4^wj{gDC-IZHMqqQ09#uX5Ga3*P8VZ8V^9>%w(5 zoqTH0n-Y{If#}$Z2`~XBkfsRm;`B6?T`VjUU;<1aIS2^Sh=F(vvY@tuVfhJX1nNUtYmvBHhXWF%R^{s0mk+9Vt{g0}KY9gcS!W-!cIv zU`^mqpX#H5bTJozl@aTVt{D2|zZ$U$&2PT^OBsP`*?)V+no(zROExJ~dKcij` zU2rfKcO8J`4~ z<9mMx26b71tIrK^&XO7SxAtM)psfha6pr2F=V0WN!_wk#XFDaHtX+>S+ipTnt$P!V znjn(m*QrpjOc6L|R|6YX;`jSxPbHpRl%|tzh?`s zT$zd{C21Xrw38F9b*R+;@6o6A#=!vnk~!1 zUY8ryJLG_h(BIr}5RYWU(8>#6G65#Q1Tq={UJsJda-7r41eibuCm`b zt(lN#MIq08@T`7ntJ?;}HKEXFcSl~83XiGfU9)dZXo5qKIZt+|HROy&3`9l5 zN{^sBz^Q0ebguOumTXvxo@b7tcGcpj-njz`(DW)}tD5-Y$_i{A+z;pfsfwD_E1`Ag z{BWc#h%JV$b!vCis^@`~r@ZlG%RXHGwgsNtqY)p+FHyg&26{^GNg6!C-E)}L>zu`_ zco7y!yI)xa7wcj44NV2B+`JX?{jm(I{%enxRx8NGH7L3ZN3rjboYtP0aQW}$n7ocg zd>*}lf!xXNAI6dEUGa5c8Nx*iHcWsCFoBFnVAhOb!&xKAh&j(`W&%tg0}~KizAmWP zsX5vgcBN~9v<%u}iY7*x6l%!r?BGnc#=<4>{n+nOz&?Zy$&gDc)*W4(k<&f`r=wcw z0$71>)v2 zH40NhyZjf~pnb6yf6_cA8J);yt!^YY&gZ9AW8BoMC{VRIhW|4Vt!ZVt->WCMb?X{_ z|7kf6K5#<47VR-+MK|eikxRE9!#{+M-LMwQNZW7at%N3?3Q~rGap3O_xbBw)wQJ>u znhxub7s-nvB={>YA3O-ngp!rYz_YRoB5v)$)-B$UiHlW|GNhfD6z-_hs1O{y{==?Y zq(+EdXbUfO&|U05{Dzb^n*X(WCr;k|7YA?ti$nK!VcmE-4uwu=e17C4?gdA0KbBNP z{EP`O0Va?>3B0A2O|6NZyT4o}AYJSWkXjM5TuguoqV@k#l|?15`(hV0i?-HWEm4WUDFZcqI=P;*@$-J zH)=#hRE(r&Q4SQeiv8k>*{J7H1Em}XU=y|Svt_S@DwQ+C=Jh?y?$`wtb2Px073SfZ z%o$ZH6h~%P6@?)NN?}Q{n-Ce{imykv7KdLDc=bNs6fTU4`LocPaU+dbR7I!0`2qhM zSkb)`D(9$6+hu)+zGKfK&`~4l717eh{F>#CPMoOB6>ioFU*-$I<#Uuc`m!o%{+ZvU zt2qDq4S8#b`i+%jM|%R>+R&+x3gmCufRrEs-p?-M>a{@9U83kkrQMu2v53Y*&sR)< z2{3_lM1ZGbrK8N`q%Z*{zyy*aAnse6vv?U4d*O_1v{hAtOrL+JX9}`HF0PXvvrjrCUY5@_n z<&<7*s3&3+YBe+(@iag2GeHe$+W$6^4zduZ87Z||XvO2fBO^#5ASFU{LP4qMICc%~ zpJj{;r`6r$khY7`+H0UB2a%y6&>3mv2q{5!baJ80s6%KdO{1cF8?AVfU?hUWXsI~4 zRoaLn269P3GSZ2M;%SMJo}-}cr?eEFIC4Po>1hp%juxbdBL@mvDXyihw~P^V8Y1oY zD}CGqa!;c+Vo#fi_79GTppy!tlPty~rcq7P%fti-M$%ctDQdL;aI_l>8-doth^HHp z(xwkbaClUjNeG$J26o~xjdZOeMMh8CUx}wJ$|*geA+!cY5+phx;x96i)5`e>Isr0j zUt$@Z4rxz^u^4F`O)MFsy;=Pp@u@kF)G0KoTf2>wHD5A;6eA!Wa#OQlODVpkOO~K@ z>()}|LW&8%5v3afE-tkGE-P)#m~K+U31I?EfC(^xqzO=?T^Alke@QzNpNb9RF#3yY z#CE%p_RS56+NM^#mkj;t#m0E_tz>URL`Yb)jag}-bsM3ib zoc?00ELSv|5)+=qR#IS$5n(YhML&{}`ye{vJs~5;L)(8D#KTJ7YGkC$ka9tPqOqiz zR33o>2nn_hgt*c1>}H4NpP2uNe`qM;QWYrN;6UpzVtnuw6JP>NAUzUDqiaLbV^(mQ zm;e)C0!)AjFaajO1eiejC%_6w`p@I!P2rB>9#wcQK=Lx>(3k)dU;<2l2`~XBkUFaajO1egF5NJ|6;xqj`*a{4PcvD?f~FAymX)whE)Kj?RvO7P7Ly4u0Vco%5+|Ui9ps{oot+)T9gf7U zx7jd(_a&gAoe-tn31`h1HavQBllKiIy}B27E(uPriQ{yAasuKmpEmSoC&r|p{YKd^ zfe$4h*3vdqTZf5-HQrIkwB$6HR{H!53f zn814zpzoQEwqN975G!)Ia^+GzdGf^7%8DiKVJ4M#7UyoL;TBWPB zabi-IfHNfC(@GCcp%kKD+On?b60Vco%m;e)C0!)AjBr^e4K$00K2gd}M zKzbtZ#L)RD&jmNfC(@GCcp%k024^p1Xuw{ z*V)U7V**To2`~XBzyz286JP@GN8oa)&5u)RE&wYd@0U01nh7ugCcp%k025#WOn?b6 zfsY`-3dl#mW3Nnr2`~XBzyz286JP>NfC;=Ofmt(#4QDOnJ)yC4Ccp%k02BDM1nN4M zypd9K0X}U?ICLh!1egF5cpCv$M&3qNfC(@GCXj3dSOG~klpGKf zU;<2l2`~XBzyz286JP>wBfttsLWElMxzwKL0whF+?U(=)U;<2l2`~XBzyz2;vJqfq zB-v1MKumxMFaajO1egF5U;<2l3A~K}DNfC*#_0yFcq|C{FmWQ@G# zR5F27Lx7c$RFf4f2oqoeOn?b60Vco%m;e(<{{&b8N&k7wDP#gnfC(@GCcp%k025#W zsfGY6AgLxRSP+qb&AG#Cc`g8#1t!1*m;e)C0!)AjWDEkVjAV>_=2S8PCcp%k025#W zOn?b6fmA_&6_8Ys4J-r`U;<2l2`~XBkZ}nt?VqPH&jrZ1`Jcg4K5NFX;jEEl@It`J zX97%s2`~XBzyz286JP=T?1z=?)!NfC(@GCcp$zkie`N!-lgKl7hfFDki`Lm;e)C0!)Aj zFaaiz(Fw2ulF>^5r=JNh0Va^K2u$8SZ;@p#K*mZsr6u$zqrq!hGQq5CIcpP}^mKo0BH$ zp$Q>AFAzY%Svj|O6y$0+ySl<&V~#_7V2BKczh5A9rUZQeYG-73v*(SVEfmxLg-IU) zzkp!aI%k5dDxv-jJp!J+h9XN&IH*31hhQ=w(9aig`%G}OQ6e(n8T?GH$mXg|ul8siu-{AQHJRaR|A7_ahbNSMrf~v-DH8rZ{!ln(hLbjJH|Nu~ z^l7{xeTevbhUP|#-y+DX9lGs2iDivSNe!K#+k4TWdQ0rj`y&EQjE_|?Pfx6e>N77% z(Rs97j?0_7!$rD#jp?55nBtw7Bpw*I;{e7tDHx?tnDAiFG}LK14bKwwG8V5M|AZ`w zJ!kuOCZO{^kIg51P_$wdl*sEGl?MVsUfsf_+mR?)xeS~&pTCtGbaN{jb((-4e;!7U za#`am1ipWuzWWsP_kM(~F3)h{3+4C(DYO}Mx1 zN0j;Q6#lusAFZjJe@b)b*SaXaq%I!coehsfBJ#U)3Wv_#hfeI=k;zf$%l0VemWbnY zY8~Kp2>UL-hS5ZCsnG~o@%rdcmTL8P%hV5|Vt8>GyU#s9SV$yHGJ&GCN0|nVQNk^` znkh*M?~nOa+%Y!aA5^D&lSrKsH(5!A7uRjfy-^p6Xy>fwf%-eSg{;g)N+vq4;asKKPlNagZrga1g)UPSks2 zJ^W$3jMWEyfo!GVNsj|=Zo+Yk_TY#X(`I7&?0NWkVqcWX?Ia0Nt=Bv(oHq;87fnU8 zl3A=85i(>g-4fFmPRFcy3o*B670JDG-ZJPn@n_7PP4-ip!$Gp;|2~$0*XmyA(4jGE z4_=0+VN!FQf;8aCndRu$YZkoxqU}C5rjPS4Q>fwSP!xEU+q9V~v}Kb4Uod!4qZt-py)r(a5KwNDjOa)v8}`(o|b z?ijXcJFecn1Ft)Gv3+V+ber)Hyu%aD(|3tq7IGKAPM?om4?>f0Eb?Ct}~Zhw$?9!tGm!Fn4e- z?0H~JTDEld^!DmLXg_v6Za#d7M^E-)T#L>ayXrD21|Oc#SI1Xj^3we4BWeJ7(5P|z78WNufY>zQVFp`Wd zWMCD%b$sIPCJY_D0(bREg_krlTL(Kh(5P;FBSYAYe=+XcS{S$P4j$aSjk^zSVB5Tb z81>JU_|DlTQ3Bq_e@2(?Q*gwK7~Z&m^(&TR`#GP~TSx1IaeDm{{JQ%F!jmZS8pmQ7 zvT!NBt(r5zZ(*W9_(AF>;O_1&q;|8i6}>;%OA1ZuRT5lea$)Q-gCugm&7DH)JoK#A z6d^F&Y$rYlIyp}$lNo}?kORWXdi2~Tzc6^so%qT0KP9xS>Mj<0q5q-yj}x>x*jI9& zt9mElY(Ro8KFZScQNBNq=S7PaNlZkJA8U?|E-PfoR$C}gy0kEM&tt*NLnqwYIZ~)v zuA6Y~g)xaJOlJ4gz5PBWp6hrP@k0x^wn?Z{rIPT++vzW_ z5K7kmUN{pRQ%0>^OP7+!apVqrab5VXc5Y$pwtJGBJ-uXtYwNW_L|o5?FyYXQCW1$e zj>3TlF?>v6uY_B-?g+t=Z%a@T&y#p$EymS(jf5P1RtfQ8Si5@rwXd(QP^wg^w>$hu z7slY{!nCFpgwFF$3i_z9d@fBA^4k;@=AQKbfY75rg`N@W<|`|VyP*4kFcR>hzc2Kv zR#E7)?V07FiONZW*oS9rnDEj>#OYE1&loR08MmQR2wn~^o z!nVsUw7mO>Bj>^Un+MYQfMu!l1g& zf@i?ebiJ88hw9T&vdjuy}5@_Fd3nzH6>>A7~@OR5OUsHSbXwLv?b0@tpJ&}7{+XS zh8rsz!$m8@*ptAC1x?^Vqc?I{QG7S^rp3K@A&WeY$?da2YikFsO=gs-GYW@a(3Fu_ zuF~_39IjVAY`{@L8VkeW}6*t=A3~b1!68NtJ_nU zG%BH>Fl8+l!(e=SPJ|y^i>-cTQKfi{=rWpurPo;sOw@x2tZSDYj)VUbMV91#_*Xl) zRU3tebZZ_1i3uaIt1!Rw_+{lqH19bYwerSPZ!&Eb6e&^=_G)vlrS94+PB=v>ic&yD zi-#1J7+&?C9{^!bF``y}{CeUXmbT1A^@R*yt@bgy@!5ykC9*+5d8yDiqt4hPRDLKZ zns+Z=Z0l~8OIEQr_IL$Jd1gtJ<-!jOhK_-%{z?q3XP&HTexyLIQk9Y0NiOB4EPMyP z)X@7Oz9S?<)j_M^8&zNG{2yazw`{5`h?=7gOLc{q&$57Zs6t_j`BJ>?7(6h^advDj$QrDG@!20JO7`xZ7s8iDLU#4F zL%ty!FtHZ7cWaMBexNCycF5gj6C%kS1%mLMyw&#v^gl$NU~7{(i{ddu%o;!wN$xYZ`rE3E#z>R4uMD(y6}bJ{&&-7q<3#HbbI@AVOVf)`n_>ViQ`#j% zzc=TWflc)}c&>Ye5#1_d=KQIsT)_MeyLbARoIS^ccE!y+tIma=88%+-9)rTY7vTP~ zrsii31<`-)BgkI-ja(F$Qf-F{gZ9!WVpLqBH2eEVZl&+2M^_ws6E4+NOIm2~^|He_ zIHA7uyxRObK=30B{$EXu9NrbhGgBT{CK>gwF|v6c3|)8I(iXZjy}UVoiYI@+^@uPe z_;Q)kn~MIA`9h~6p;(l+5TND)w;4xsH3-Cb!d`?dWgB_Z7?FY~BB^dR)4~Y_8sl`b3VtG>(8GaL6LIjwo zoCr5&V|G;;IEf^rZv9dPbE-N+ zHa0WmlL6uPW*}&8f2_GqH9Ez4aG*VQv^S7jBaFgb^uI9}ZPx_H2$9Iuy(=dXQMCk0 zSe{dbY6^$%tCx87@+Drpcu5PB&3S8#3`3YMM!^w`kqC>>Sw5ncAd%sFJ@9%)#KLkW>5!&P(DXexMEBu zY#Ceu3xbA8aS4JB-FNoHpJBN$=CxaYJNC?w>9$fZmOI;E3M`*KJa`pX@(T5swPo>pa!vS_f#w;^4gl74v-w?axnO z5;+I~80@+grCLnDlVGu7POP6k!IpLB(QP+9zcdO1mKyQs*S5%AZ>p3RUb7pMLK#^J_W)#~ioqBWhKE0ofdA+K2tMmjo7(wC zT^Pbi!4?}x;s5=JTy4)`@gswjSGN~+!nO5lkY&(WgdZLVIJCe?3h(u<=_ux=Nb>y% z@!EloU(G?&zn@F*&eZ}xq1PH4+VmxE`or<^{$uz#7D6$*cn_l-8>ul}ANBuM;iQhr zk--SlsgW2}R*fOs10*+sLJ9pZ4ba^9r^Mo5*D#D7w*evK-W2`{bJ{hhLz{UclZM8u4zjOYb#_Lc9EF9z}QKCF{cxmXUdTEkE6khOBb6R{4UzZ*mRP^0`HsuN`B zvo!>(dVh)f6ZU{crEy|VAw*D~$uAwnj03LN@_^o}(?j7#Yo{V2!l5IDLoBQJ=9Gu$ z?7}!le#ElBM79IJ-iq#AlcG(v|FJTXYO^LO!HkFqDuAyScSBxy;g5wEptDn3J{D%5 zRN-IUjcq$$pxietQQBS!wYCYGx1);o>_r^+dTiLJL} zc?Py`+ltNWR-k>e0su7v=AKHuJ%}H|D@i{l9RgGpv3X$!9O%;we}pJ$M>O#hFB+-( z!R$%9&~nxmRIwu~s(?^zD9(<_h8^SQ<33p_OkoJg+8# zA#2_u$Xc}-jxFsd-OJR~QJ}atF5Y@5RoY)J2*IKpTd{7#CT!bt1unTAv1NBsBXMFd zIy}7_3R9s1u=Horj%)V5hcL2BJG5xg0?nJY#fSyF5Qv0&2PIM7>pzi;ldTq+9PJbF z=Ytu;e@C&o7tt}hh>Lt=Yl}0pOXAFki8w>P)U^%MX3% z{s1OTy@h7qm&A!J8?kZgZYWBA0nnWDebL1-s|PA-Z^BE2pC*iqMk{==f7vK>XxW^xr03LPmK`tZ;wWra2%II$4{jyYlPbqbp{ZN%n(uONHjytsb)DuQqsYgWq8 zX2lPntOoKl$I9Wgs2+?_axHOF?I||5^Jk|8*a?~l>{_}4+HW>vX0@o-NRbcIC-jEu zpGDXZM4K#7ofn$77#1#`h9c5{iYLD8Y{cpPXDP%dSTOM*8jh}m$D4`a=4~{3-~y*t zm$8dBnn-nqIvV`$9K)tPd$4EKAhe$siZ+YqppM3bGn?1KuhLX3ZX#AVqH)5vgGZs% znRQtE*kAe%8EBNGQJ3LpT7dFYG6F(rG*|3Y2o+m_2^tmT@`D)Opa~jQFNq4{Z(+uX zi}=2fsP*V@Woe+o2RK=ZZ&%*WtvOdnnYg6FS!N zgxE(H5w52_F!a>9VHGKTf3jDfgz5EZJ*2rZje1D$0*%M)*|ZO(e_Vj?#5#hWWFuvK z*uaj+B)X#T#5H>1mKJ*w?}s<=`tAXQo+0eMR7U!sZh$@BL5RQPt-{JPMW>Q$TPUTUsPFbv&(+Boc#!UKJsMshFXHVax z=Ql8A#0fN?P#ZU;dUOYT>F0spkJ#GlmTplffnp5-{oM%U*CG z<~3i0mJ5bpb>klqYmu8l53eIzag#cO%U<~x!$%#Z)~Fu0?~AY2b!I(g5JvLvR1UIiaI15`;tkKe8DMuPB8LAm2L+)h8Fo+#z;z++V ze4>v;U?_bWGuu$>Tu)t%aww@wP5d5S{Jjr)&bo#wZR)^T7J*yOshh?9?V5v3K|(%z zC3Ue$IciM-|dI>-@U8VYe* zqM#^*Y15jZLUf}{3ONQf-KKIQ(9l3uwE1xfR`o4MV;&pPv}GIoZ%sq2=t!j_;1!-} zePsH%}lG8CkXg;(${FVw+UL5>3|{Q>q+G*Uc#x{)>y3w ziijFVTqAx}pPa+(~+WLzU?J??LpaAU1=1@#x-Dcoygc6U{k`HmdCT;pXl^b1lU> zh|=b4hfHPiN?Jg)o45b!h)Zj%JFx`Csg3u~z=|0B<1&1)wl^}9RutjqkLUhRaq;Xa zxTr|85c9fdTl`r43)sLJJ=d(j%jL%~rqeKJYE8z-Q5{jUKtg#*l8}rxbxV^!^L31D zVg>mUS8u(9YhhP<@2Ogdt}|mC#lA}N{V*cv#VdMAazP_imogY5Ll8>C(D8$%9wujd zct3goUxN-g5*m>6xr7I=UC^_5^u~bZP)b&53DY(6wu9oFG(-kjWHeGG)!68n6EM#O z5a-c}^MOp>FW_wo!Ob&A5ot$$#jI1+#@vZjkULi0YzZojUYHpPv`5d3-TN`adl%Y# z@f{{D*^cs4TBG6E0mxf+F1FJUg>lD0WNSAbH8>o}ToL;dQ zq$QKCid2-p^ok1!|!A^cBt8YFbe+j6J~DQgu>4*(F8Ir8n&-11!{TEXw2X8Ig1E2 zcTdw8zmXcic3N{{>QCB@$zR&hgh#5py}&A?DEihjIOu!gHN~ zMNQ429P!zVjg~IK#6qTn8Qe9HpAVoR51P^;Iiq|er8p#rJkwzxF~$x#bCc|v7&|+y z&D$FSF#$+dw)W*vv($6!*n1Mg8nlaQ5?VX5fgQa%>yACaL{AT?%!wcGCr8!*MgNCl z(`;YA#pyS{gb|@FQIqtvb15-qx8oJ8=st+KBR5J5<45 zslw*tv`1!-eA25YJ`KBZ0&g5QG%yn>p|OgC zwFA1AQG<=T*~r+UQ=g$2U27N?o$iB)RphYAX6LIgwNAj*cV8qovp%V{N2z zYF*~7AEvGA=sdHe#=SOr{I4X&-Fc31s^pwYjYNm44p=c|HEexyW8kog)Hx9QjZfAT zi<+Lo4LPZ^PyW?-OlqA#)0_6i601P5U|HlAg&<3DzG*x|BPY&k<^K&2yeeqXd8W5j{^ebDXK3v^upoqH*;x!W{p1S{;sT(lZ- zfaZuL{MoBAWi!6@+lsH+j=7@7a42FH95HqH0%}xKURUi1 z55G;Aw#6$dbOA>e^+wla*CD$-AI|-EN%n5VtHaHaMu#26&KYXY6)1?p!RJAno4kV& zs6;!X>)^KVn%xh-(&tIcYwyE3%@KbpFCOWvJ-VYT#iZp=J<83rE zK>XIpu#CtU4tfv-# zjO&N%R3}R=PW*r-%XR28sJWD0QN)Ce5%zBF(7M(e%$T~4<}8{`Zl++GbmEUQ(UJ?#vX0jH^A1Jy>UQS5_JnDaL`p0pXB+Wx{bQG z#xAIbt?ip&pLi=8`u2EyIRy{IYmE)E=a9qrB5J0CXqS|sQF^hC`)N*Ptm)hVr|3@n zobBq}5%aE^)%c{mr-o+<*TktoGL|7h0#YXh{byatwEt-qJOVD@f%wIyZXW6w#MKSI z(R9H~sO(uEg>tIl;NVDq4sgy_5Vb4UqG9N#Sh?Kjb}R?+Zpt*$S|wmn4!}bWVf5wpy8N4#-Gd5x-Oyx5BYQ*MsK~Nj+$zXZNS7L5W&zJD^I% ztWaMVg~~L%C^i&jPA##3dam_Jb)m(RG3{C?wX`zdkwyWml9272e=w^t&4lvE17~~l zld9A2;Nl=h)Nql(hUVnS%Iv@ze*t64n{!2>aipVpV}I=w8V9&gO)P$(MI)n?>rQmc zwSz-sIK>m6oapTr%1TL;YY;0%*O@kb57NCCLWhhMpMtNwv zn#fJ9@pbMfb{oW)9z1v zn}uA<@}k>-_INbE9MnoV#-E|_;5>D(Y4c><7+XQgYmI#tO!w=AmA$C+kqBS@;)nUA zr%PM%6`bLZ#XpaNySqcI_bXZsrEPiq{^TVFI&MHATN-(#vZJNZ#*@vTVq8^f)Ycde z|LcpevZIiPR4F|?lf=Wydv9Y5QoU|lcw9(kRQsalFVv&VQ+f@iyn*$wO zC~P&cF0~LtW(+|E$Jp;_wBOJ9E7n6!wCFG)qbd6>u3lM(Geh%9j8v)u*lIrx$A0}r zoYhH22CcmdZA}+l)0jiaQGGZXX8Idt>6Fq|OKzD*O{^l2sqNwFLUn`1PK#z;ze1*K zTd-SfpLx{4x=qt?cT!bzUQ-<~;blks)w2XpWDt(H^?{tql1iNu8?+6vpl@v&Q=zFd zB}b!w-K%Iu+YQwmwF$vwrzXG7+Su|%*D2yVuB|;0&$rjah@x7WQl&=O;agCqRVUD( z7m#-##((3BZ{1}m-+ewF(=64*-w!JDg&JbfuRW*{nup4X3`@Uwf&ZqJlcwjzyag1l zWHe|vopzYn(F;rJQ{6>nQ1tV~UP}A0M&v??mP_wpb={{ZNh<}#x?f%Q5_SXzpm#BI z1*FtEQ{5kvcXG8YoSp2c-ly2KnK5wXZq#@@6fU$AhYi)I?kj8H_HQkrimJb&1WDUJ zS!-eYkj&^=DwD*}u0S_9&pv@w-&d8&gqXOT?M9-F3$3rAPOvPlQDuQT6>RApwzVp| z=65(|4!hFURXVsz3`4FmvyJ|qnZFfI(zn8#xAZPJxJq@aSO?_ju@_H&F`)sy|29-< z$f_TtMVCX7PYTQ#wL0{M)0zL#^k-VJzS~4)kLnbAs#ECOV=+`;+tPR1PNR^%NBM`G z!nJYFP?hQ*ar?8O+P3Ih%xa#n#Uo#&Y65BDz>wYJD+`{j`w9PEwc5zvpcf7<=pj@kf5^#8;P&}L13~&(^K8OkH#-bX6EsY#HHd-Z| z`fH-l?u##kFRFAAW}Y!`C2!eV+xNDxdi)rn&X->bU)E_VwC*-f*m)x`;l12rg8!^n z$$4OiB_S3d7MrMxq&6Rq+TK1PzX{pJ4La6+;_2{@nCiFF@D8ho(Qw*R~8!2I&L{%z$)m_!u?|f$tE-GE`?90lbC`YtRR@NzX`F;n6SV2or=EOKy z`HM*!y}y%=Zw@Xe3NK9<|)2oiB`6^_N5<%0Z**VgyZ!F`I~ghc$H zxTEM;`88iA6hQ(BvGSL|Q^_iA9T<{Gx~;4O@2wZ=7b+&qJ(AcFHe#}^JcyUpoUE@U zLT~v`3d$VGlkHrH+VmcdAwQ9h^MH6T^;S{1eYNM^Fr%{PI1OCRD0nt_h>W({& zy3x0Be%-5g#|rwhHi9$Fm0Y+6 zTKkQW$q4j-wIvI3;Qlk0!Pk>qxDyEw0`*Ad3g9i;wY3i`LKV!1je&;M6fbYAdhy;f$>v;%n>Q3sT&w7CqaP&pJlfqm{+fe}O4mF3 ziRDa^BbkJW74WCHv63^IW2|Tr<6z}4%6)>~V}+G~!)Q;@zmk*0;-rVM+*t=6Umo+* z7<-FX%SBS*S%;lW*D<2R1ZXWwjO|;lqWD<(HD4waK>|Tp`Agtw8nj9z-B#AemyX1J zr#5I>!!3#QS$U8yt?#X_6AwYInY0avdqzOquoCy5VV`|z0y$q008SJtYKpAoE0vjg41egF5 zNVNoVc{ZReBiMXS0<3_1PWtIMZl@Bhu={aBE8GN>7<>l8;HbpEeco3(ro>5O0-u(^ zSGBv-4is_c0({yyIdmq#1egF5U;^($;KL{DzE3RdmI+u0#F@4g=hotkFPH!mU;<2l z2`~XBzy#7I0aifLWwvsnm;e)C0!)AjFaajO1eic7B`|Bou;HIf3vrlu>?O|yNToT? zVle?Gzyz286JP>NfC(^xR6yX9DI=*MZ7c#4U;<2l2`~XBzyz286JP?Vi~uVjsVqZS z945d7m;e(<%LFdl3QKq{Kw8d0mYoSOfs`e{%1Fxc=Gd436JP>NfC(@GCcp%kK!zs3 z3P^^|eoj0SU;<2l2`~XBzyz286G&MCtbn8}@05sboVwd?o(qr?q&W^Izyz286JP>N zfC(^xk0QXz$VcI0&rE;`FaajO1egF5U;<2l3A_&hRzTi|7Q1BvOn?b60Vco%QYV1| z;lb5-EoBPPHEm;e)C0!)AjFaajO1d^8kD&joltX6%{?FaajO1egF5$j}64%@{VE zHIfWn1UT_bfC(@GCcp%k025#WOdw?mumX~@yg4=|zyz286JP>NfC;1*0viMS)#teY z=_NNmQ#x1~`ApzBS|-2*m;e)C0!)AjFaaiz5ecvYk`Z&A)64{z025#WOn?b60Vco% zJ`({}Kt2N zfC(@GCcp$TFacITGH~W|(wP7gU;<2l2`~XBzy#7F0l~qoO!}A$z{*HkEPpIB6JP>N zfC(@GCcp%k024@~1Xuw{qZ!9yGXW;R1egF5U;<2l2{3`QNPrcPw3urwGZSC}OyKhq zD6y+>^Yk$n;PbO&377yAU;-JI04pOIHtRXzOn?b60Vco%m;e)C0!-jD5STS%*l^ZD zJ_C4;k_j*YCcp%k025#WOn?bwR03&L0hyM+pgYe6$f%{3)6E2!025#WOn?b60Va?x z2&7eIBwfJgL@)s+zyz286JP>NfC(^xj7ESJkc^h&oL(lt1egF5NV^0Um-xE|&jm=k z`Nxto0VWWS04pQ$kgx?4U;<2l2`~XBzyz286JP?LiU2DhpNcGpn;{9v<@`!z$c*Pi zGXW;R1egF5U;^(Ne~1)eE0|^lkr`>u{$OZi$GvNAapt%e0+Qm6ciNe8Q(C0cO~HL0=7l zJBRX-4#iggYx=mJKYw0Q0>nle8zzum2$Zc_9Hfb)m*j9dQka0jV8p!}k5Rl_K^W