mirror of https://github.com/kubevela/kubevela.git
Feat: add the CloudShell feature (#4280)
* Feat: add the CloudShell feature Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: unit test bug Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Feat: handle the error Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Feat: change the auth package Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: change the CSR name Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: change the generate function Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: unit test Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: e2e test Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
This commit is contained in:
parent
2af8ab13d6
commit
eea8570e10
|
|
@ -170,6 +170,25 @@ jobs:
|
|||
docker.io/oamdev/vela-rollout:${{ steps.get_version.outputs.VERSION }}
|
||||
ghcr.io/${{ github.repository_owner }}/oamdev/vela-rollout:${{ steps.get_version.outputs.VERSION }}
|
||||
${{ secrets.ACR_DOMAIN }}/oamdev/vela-rollout:${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
- uses: docker/build-push-action@v2
|
||||
name: Build & Pushing CloudShell for Dockerhub, GHCR and ACR
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.cloudshell
|
||||
labels: |-
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
build-args: |
|
||||
GITVERSION=git-${{ steps.vars.outputs.git_revision }}
|
||||
VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||
GOPROXY=https://proxy.golang.org
|
||||
tags: |-
|
||||
docker.io/oamdev/cloudshell:${{ steps.get_version.outputs.VERSION }}
|
||||
ghcr.io/${{ github.repository_owner }}/oamdev/cloudshell:${{ steps.get_version.outputs.VERSION }}
|
||||
${{ secrets.ACR_DOMAIN }}/oamdev/cloudshell:${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
publish-charts:
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
bin
|
||||
_bin
|
||||
e2e/vela
|
||||
vela
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
|
@ -49,4 +50,4 @@ tmp/
|
|||
git-page/
|
||||
|
||||
# e2e rollout runtime image build
|
||||
runtime/rollout/e2e/tmp
|
||||
runtime/rollout/e2e/tmp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
ARG BASE_IMAGE
|
||||
# Build the cli binary
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.17-alpine as builder
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-https://goproxy.cn}
|
||||
WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
# cache deps before building and copying source so that we don't need to re-download as much
|
||||
# and so that source changes don't invalidate our downloaded layer
|
||||
RUN go mod download
|
||||
|
||||
# Copy the go source
|
||||
COPY apis/ apis/
|
||||
COPY pkg/ pkg/
|
||||
COPY version/ version/
|
||||
COPY references/ references/
|
||||
|
||||
# Build
|
||||
ARG VERSION
|
||||
ARG GITVERSION
|
||||
|
||||
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -a -ldflags "-s -w -X github.com/oam-dev/kubevela/version.VelaVersion=${VERSION:-undefined} -X github.com/oam-dev/kubevela/version.GitRevision=${GITVERSION:-undefined}" \
|
||||
-o vela ./references/cmd/cli/main.go
|
||||
|
||||
FROM ghcr.io/cloudtty/cloudshell:v0.2.0
|
||||
RUN apt-get install -y vim
|
||||
ENV API_TOKEN_PATH=/usr/local/kubeconfig/token
|
||||
COPY --from=builder /workspace/vela /usr/local/bin/vela
|
||||
11
go.mod
11
go.mod
|
|
@ -17,6 +17,7 @@ require (
|
|||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
|
||||
github.com/briandowns/spinner v1.11.1
|
||||
github.com/chartmuseum/helm-push v0.10.2
|
||||
github.com/cloudtty/cloudtty v0.2.0
|
||||
github.com/containerd/containerd v1.5.13
|
||||
github.com/coreos/go-oidc v2.1.0+incompatible
|
||||
github.com/coreos/prometheus-operator v0.41.1
|
||||
|
|
@ -47,6 +48,7 @@ require (
|
|||
github.com/hashicorp/hcl/v2 v2.9.1
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/kubevela/prism v1.4.1-0.20220613123457-94f1190f87c2
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.1
|
||||
|
|
@ -84,7 +86,7 @@ require (
|
|||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gotest.tools v2.2.0+incompatible
|
||||
helm.sh/helm/v3 v3.7.2
|
||||
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02
|
||||
istio.io/client-go v1.13.4
|
||||
k8s.io/api v0.23.6
|
||||
k8s.io/apiextensions-apiserver v0.23.6
|
||||
k8s.io/apimachinery v0.23.6
|
||||
|
|
@ -109,6 +111,8 @@ require (
|
|||
|
||||
require (
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
|
|
@ -179,7 +183,6 @@ require (
|
|||
github.com/fluxcd/pkg/apis/meta v0.13.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.1 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-logr/zapr v1.2.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
|
|
@ -305,8 +308,8 @@ require (
|
|||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
istio.io/api v0.0.0-20210128181506-0c4b8e54850f // indirect
|
||||
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a // indirect
|
||||
istio.io/api v0.0.0-20220512212136-561ffec82582 // indirect
|
||||
istio.io/gogo-genproto v0.0.0-20211208193508-5ab4acc9eb1e // indirect
|
||||
oras.land/oras-go v0.4.0 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy v0.0.30 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect
|
||||
|
|
|
|||
20
go.sum
20
go.sum
|
|
@ -388,6 +388,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
|
|||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudtty/cloudtty v0.2.0 h1:QvDbb2hZl7nSfLDrKkbjIixjkW6seSVBX3N/GRmioyM=
|
||||
github.com/cloudtty/cloudtty v0.2.0/go.mod h1:RRVb8fLrfpzjsLFqaUk74ouRvZ2drVCvSN3ZzidHju8=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
|
|
@ -1336,6 +1338,8 @@ github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHU
|
|||
github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
|
||||
github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw=
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
|
@ -2927,7 +2931,6 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
|
|||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
|
|
@ -3058,12 +3061,12 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
|||
honnef.co/go/tools v0.2.0/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
|
||||
honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
istio.io/api v0.0.0-20210128181506-0c4b8e54850f h1:zUFsawgPj5oI9p5cf91YCExRlxLIVsEkIunN9ODUSJs=
|
||||
istio.io/api v0.0.0-20210128181506-0c4b8e54850f/go.mod h1:88HN3o1fSD1jo+Z1WTLlJfMm9biopur6Ct9BFKjiB64=
|
||||
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02 h1:ZA8Y2gKkKtEeYuKfqlEzIBDfU4IE5uIAdsXDeD41T9w=
|
||||
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02/go.mod h1:oXMjFUWhxlReUSbg4i3GjKgOhSX1WgD68ZNlHQEcmQg=
|
||||
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a h1:w7zILua2dnYo9CxImhpNW4NE/8ZxEoc/wfBfHrhUhrE=
|
||||
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a/go.mod h1:OzpAts7jljZceG4Vqi5/zXy/pOg1b209T3jb7Nv5wIs=
|
||||
istio.io/api v0.0.0-20220512212136-561ffec82582 h1:AzLIET6ePAqxlWaXA6GOzapoRX1GRC6mZ8GY+cQIWYU=
|
||||
istio.io/api v0.0.0-20220512212136-561ffec82582/go.mod h1:8ZZgyVgYrHhsFQarEgTfPnMGpdgTDZbxSjYhdwTUuAQ=
|
||||
istio.io/client-go v1.13.4 h1:QJBFBkOaplyL/uBL7xo75mdE5G0i1uR6BR0u9/Wuo1E=
|
||||
istio.io/client-go v1.13.4/go.mod h1:kM3WH/HCojq7BhCD894SZuaAXUKMswT+VQRaEEhTGj0=
|
||||
istio.io/gogo-genproto v0.0.0-20211208193508-5ab4acc9eb1e h1:z2WI3y55w0K3c6hmarcp5EcOiP4vVpTBXA8nYstP+cE=
|
||||
istio.io/gogo-genproto v0.0.0-20211208193508-5ab4acc9eb1e/go.mod h1:vJDAniIqryf/z///fgZqVPKJ7N2lBk7Gg8DCTB7oCfU=
|
||||
k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=
|
||||
k8s.io/api v0.0.0-20190813020757-36bff7324fb7/go.mod h1:3Iy+myeAORNCLgjd/Xu9ebwN7Vh59Bw0vh9jhoX+V58=
|
||||
k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48=
|
||||
|
|
@ -3075,7 +3078,6 @@ k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
|
|||
k8s.io/api v0.17.5/go.mod h1:0zV5/ungglgy2Rlm3QK8fbxkXVs+BSJWpJP/+8gUVLY=
|
||||
k8s.io/api v0.18.0-beta.2/go.mod h1:2oeNnWEqcSmaM/ibSh3t7xcIqbkGXhzZdn4ezV9T4m0=
|
||||
k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8=
|
||||
k8s.io/api v0.18.1/go.mod h1:3My4jorQWzSs5a+l7Ge6JBbIxChLnY8HnuT58ZWolss=
|
||||
k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78=
|
||||
k8s.io/api v0.18.3/go.mod h1:UOaMwERbqJMfeeeHc8XJKawj4P9TgDRnViIqqBeH2QA=
|
||||
k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI=
|
||||
|
|
@ -3126,7 +3128,6 @@ k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZ
|
|||
k8s.io/apimachinery v0.17.5/go.mod h1:ioIo1G/a+uONV7Tv+ZmCbMG1/a3kVw5YcDdncd8ugQ0=
|
||||
k8s.io/apimachinery v0.18.0-beta.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
|
||||
k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
|
||||
k8s.io/apimachinery v0.18.1/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
|
||||
k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
|
||||
k8s.io/apimachinery v0.18.3/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
|
||||
k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
|
||||
|
|
@ -3186,7 +3187,6 @@ k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI=
|
|||
k8s.io/client-go v0.17.5/go.mod h1:S8uZpBpjJJdEH/fEyxcqg7Rn0P5jH+ilkgBHjriSmNo=
|
||||
k8s.io/client-go v0.18.0-beta.2/go.mod h1:UvuVxHjKWIcgy0iMvF+bwNDW7l0mskTNOaOW1Qv5BMA=
|
||||
k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8=
|
||||
k8s.io/client-go v0.18.1/go.mod h1:iCikYRiXOj/yRRFE/aWqrpPtDt4P2JVWhtHkmESTcfY=
|
||||
k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU=
|
||||
k8s.io/client-go v0.18.3/go.mod h1:4a/dpQEvzAhT1BbuWW09qvIaGw6Gbu1gZYiQZIi1DMw=
|
||||
k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q=
|
||||
|
|
|
|||
|
|
@ -1651,3 +1651,55 @@ func compare(ctx context.Context, c common2.Args, newApp *v1beta1.Application, o
|
|||
reportDiffOpt.PrintDiffReport(diffResult)
|
||||
return diffResult, buff, nil
|
||||
}
|
||||
|
||||
// NewTestApplicationService create the application service instance for testing
|
||||
func NewTestApplicationService(ds datastore.DataStore, c client.Client, cfg *rest.Config) ApplicationService {
|
||||
targetImpl := &targetServiceImpl{K8sClient: c, Store: ds}
|
||||
envImpl := &envServiceImpl{KubeClient: c, Store: ds}
|
||||
rbacService := &rbacServiceImpl{Store: ds}
|
||||
userService := &userServiceImpl{Store: ds, RbacService: rbacService, SysService: systemInfoServiceImpl{Store: ds}}
|
||||
projectService := &projectServiceImpl{
|
||||
K8sClient: c,
|
||||
Store: ds,
|
||||
RbacService: rbacService,
|
||||
TargetService: targetImpl,
|
||||
UserService: userService,
|
||||
EnvService: envImpl,
|
||||
}
|
||||
userService.ProjectService = projectService
|
||||
envImpl.ProjectService = projectService
|
||||
workflowService := &workflowServiceImpl{
|
||||
Store: ds,
|
||||
KubeClient: c,
|
||||
Apply: apply.NewAPIApplicator(c),
|
||||
EnvService: envImpl,
|
||||
}
|
||||
def := &definitionServiceImpl{KubeClient: c}
|
||||
envbinding := &envBindingServiceImpl{
|
||||
Store: ds,
|
||||
WorkflowService: workflowService,
|
||||
EnvService: envImpl,
|
||||
DefinitionService: def,
|
||||
KubeClient: c,
|
||||
}
|
||||
workflowService.EnvBindingService = envbinding
|
||||
return &applicationServiceImpl{
|
||||
Store: ds,
|
||||
KubeClient: c,
|
||||
KubeConfig: cfg,
|
||||
Apply: apply.NewAPIApplicator(c),
|
||||
WorkflowService: &workflowServiceImpl{
|
||||
Store: ds,
|
||||
KubeClient: c,
|
||||
Apply: apply.NewAPIApplicator(c),
|
||||
EnvService: envImpl,
|
||||
EnvBindingService: envbinding,
|
||||
},
|
||||
EnvService: envImpl,
|
||||
EnvBindingService: envbinding,
|
||||
TargetService: targetImpl,
|
||||
DefinitionService: def,
|
||||
ProjectService: projectService,
|
||||
UserService: userService,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,365 @@
|
|||
/*
|
||||
Copyright 2022 The KubeVela 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1alpha1 "github.com/cloudtty/cloudtty/pkg/apis/cloudshell/v1alpha1"
|
||||
"github.com/ghodss/yaml"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
kubevelatypes "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultCloudShellPathPrefix the default prefix
|
||||
DefaultCloudShellPathPrefix = "/view/cloudshell"
|
||||
// DefaultCloudShellCommand the init command when open the TTY connection
|
||||
DefaultCloudShellCommand = "bash"
|
||||
// DefaultLabelKey the default label key for the cloud shell
|
||||
DefaultLabelKey = "oam.dev/cloudshell"
|
||||
|
||||
// StatusFailed means there is an error when creating the required resources, should retry.
|
||||
StatusFailed = "Failed"
|
||||
|
||||
// StatusPreparing means the required resource is created, waiting until the environment is ready.
|
||||
StatusPreparing = "Preparing"
|
||||
|
||||
// StatusCompleted means the environment is ready.
|
||||
StatusCompleted = "Completed"
|
||||
)
|
||||
|
||||
// CloudShellService provide the cloud shell feature
|
||||
type CloudShellService interface {
|
||||
Prepare(ctx context.Context) (*apisv1.CloudShellPrepareResponse, error)
|
||||
GetCloudShellEndpoint(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// GenerateKubeConfig generate the kubeconfig for the cloudshell
|
||||
type GenerateKubeConfig func(ctx context.Context, cli kubernetes.Interface, cfg *api.Config, writer io.Writer, options ...auth.KubeConfigGenerateOption) (*api.Config, error)
|
||||
|
||||
type cloudShellServiceImpl struct {
|
||||
KubeClient client.Client `inject:"kubeClient"`
|
||||
KubeConfig *rest.Config `inject:"kubeConfig"`
|
||||
UserService UserService `inject:""`
|
||||
ProjectService ProjectService `inject:""`
|
||||
RBACService RBACService `inject:""`
|
||||
TargetService TargetService `inject:""`
|
||||
EnvService EnvService `inject:""`
|
||||
GenerateKubeConfig GenerateKubeConfig
|
||||
}
|
||||
|
||||
// NewCloudShellService create the instance of the cloud shell service
|
||||
func NewCloudShellService() CloudShellService {
|
||||
return &cloudShellServiceImpl{
|
||||
GenerateKubeConfig: auth.GenerateKubeConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare prepare the cloud shell environment for the user
|
||||
func (c *cloudShellServiceImpl) Prepare(ctx context.Context) (*apisv1.CloudShellPrepareResponse, error) {
|
||||
res := &apisv1.CloudShellPrepareResponse{}
|
||||
var userName string
|
||||
if user := ctx.Value(&apisv1.CtxKeyUser); user != nil {
|
||||
if u, ok := user.(string); ok {
|
||||
userName = u
|
||||
}
|
||||
}
|
||||
if userName == "" {
|
||||
return nil, bcode.ErrUnauthorized
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*20)
|
||||
defer cancel()
|
||||
var cloudShell v1alpha1.CloudShell
|
||||
var shouldCreate bool
|
||||
if err := c.KubeClient.Get(ctx, types.NamespacedName{Namespace: kubevelatypes.DefaultKubeVelaNS, Name: makeUserCloudShellName(userName)}, &cloudShell); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
shouldCreate = true
|
||||
} else {
|
||||
if meta.IsNoMatchError(err) {
|
||||
return nil, bcode.ErrCloudShellAddonNotEnabled
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
if shouldCreate {
|
||||
if err := c.prepareKubeConfig(ctx); err != nil {
|
||||
return res, fmt.Errorf("failed to prepare the kubeconfig for the user: %w", err)
|
||||
}
|
||||
new, err := c.newCloudShell(ctx)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if err := c.KubeClient.Create(ctx, new); err != nil {
|
||||
if meta.IsNoMatchError(err) {
|
||||
return nil, bcode.ErrCloudShellAddonNotEnabled
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
res.Status = StatusPreparing
|
||||
} else {
|
||||
if cloudShell.Status.Phase == v1alpha1.PhaseFailed {
|
||||
if err := c.KubeClient.Delete(ctx, &cloudShell); err != nil {
|
||||
log.Logger.Errorf("failed to clear the failed cloud shell:%s", err.Error())
|
||||
}
|
||||
res.Status = StatusFailed
|
||||
}
|
||||
if cloudShell.Status.Phase == v1alpha1.PhaseReady {
|
||||
res.Status = StatusCompleted
|
||||
} else {
|
||||
res.Status = StatusPreparing
|
||||
res.Message = fmt.Sprintf("The phase is %s", cloudShell.Status.Phase)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *cloudShellServiceImpl) GetCloudShellEndpoint(ctx context.Context) (string, error) {
|
||||
var userName string
|
||||
if user := ctx.Value(&apisv1.CtxKeyUser); user != nil {
|
||||
if u, ok := user.(string); ok {
|
||||
userName = u
|
||||
}
|
||||
}
|
||||
if userName == "" {
|
||||
return "", bcode.ErrUnauthorized
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||
defer cancel()
|
||||
var cloudShell v1alpha1.CloudShell
|
||||
if err := c.KubeClient.Get(ctx, types.NamespacedName{Namespace: kubevelatypes.DefaultKubeVelaNS, Name: makeUserCloudShellName(userName)}, &cloudShell); err != nil {
|
||||
if meta.IsNoMatchError(err) {
|
||||
return "", bcode.ErrCloudShellAddonNotEnabled
|
||||
}
|
||||
if apierrors.IsNotFound(err) {
|
||||
return "", bcode.ErrCloudShellNotInit
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return cloudShell.Status.AccessURL, nil
|
||||
}
|
||||
|
||||
// prepareKubeConfig prepare the user's kubeconfig
|
||||
func (c *cloudShellServiceImpl) prepareKubeConfig(ctx context.Context) error {
|
||||
var userName string
|
||||
if user := ctx.Value(&apisv1.CtxKeyUser); user != nil {
|
||||
if u, ok := user.(string); ok {
|
||||
userName = u
|
||||
}
|
||||
}
|
||||
if userName == "" {
|
||||
return bcode.ErrUnauthorized
|
||||
}
|
||||
user, _ := c.UserService.GetUser(ctx, userName)
|
||||
if user == nil {
|
||||
return bcode.ErrUnauthorized
|
||||
}
|
||||
projects, err := c.ProjectService.ListUserProjects(ctx, userName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var groups []string
|
||||
for _, p := range projects {
|
||||
permissions, err := c.RBACService.GetUserPermissions(ctx, user, p.Name, false)
|
||||
var readOnly bool
|
||||
if err != nil {
|
||||
log.Logger.Errorf("failed to get the user permissions %s", err.Error())
|
||||
readOnly = true
|
||||
} else {
|
||||
readOnly = checkReadOnly(p.Name, permissions)
|
||||
}
|
||||
if readOnly {
|
||||
if err := c.managePrivilegesForUser(ctx, p.Name, true, userName, false); err != nil {
|
||||
log.Logger.Errorf("failed to privileges the user %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
groups = append(groups, utils.KubeVelaProjectGroupPrefix+p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if utils.StringsContain(user.UserRoles, "admin") {
|
||||
groups = append(groups, utils.KubeVelaAdminGroupPrefix+"admin")
|
||||
}
|
||||
|
||||
cli, err := kubernetes.NewForConfig(c.KubeConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cfg.Clusters) == 0 {
|
||||
caFromServiceAccount, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
|
||||
if err != nil {
|
||||
log.Logger.Errorf("failed to read the ca file from the service account dir,%s", err.Error())
|
||||
return err
|
||||
}
|
||||
cfg.Clusters = map[string]*api.Cluster{
|
||||
"local": {
|
||||
CertificateAuthorityData: caFromServiceAccount,
|
||||
Server: "https://kubernetes.default:443",
|
||||
},
|
||||
}
|
||||
}
|
||||
for k := range cfg.Clusters {
|
||||
cfg.Clusters[k].Server = "https://kubernetes.default:443"
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
cfg, err = c.GenerateKubeConfig(ctx, cli, cfg, buffer, auth.KubeConfigWithIdentityGenerateOption(auth.Identity{
|
||||
User: userName,
|
||||
Groups: groups,
|
||||
}))
|
||||
if err != nil {
|
||||
log.Logger.Errorf("failed to generate the kube config:%s Message: %s", err.Error(), strings.ReplaceAll(buffer.String(), "\n", "\t"))
|
||||
return err
|
||||
}
|
||||
bs, err := clientcmd.Write(*cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cm := corev1.ConfigMap{}
|
||||
cm.Name = makeUserConfigName(userName)
|
||||
cm.Namespace = kubevelatypes.DefaultKubeVelaNS
|
||||
cm.Labels = map[string]string{
|
||||
DefaultLabelKey: "kubeconfig",
|
||||
}
|
||||
identityByte, _ := yaml.Marshal(auth.Identity{
|
||||
User: userName,
|
||||
Groups: groups,
|
||||
})
|
||||
cm.Data = map[string]string{
|
||||
"config": string(bs),
|
||||
"identity": string(identityByte),
|
||||
}
|
||||
|
||||
// mount the token for requesting the API
|
||||
if tokenV := ctx.Value(&apisv1.CtxKeyToken); tokenV != nil {
|
||||
if u, ok := tokenV.(string); ok {
|
||||
cm.Data["token"] = u
|
||||
}
|
||||
}
|
||||
|
||||
var exist = &corev1.ConfigMap{}
|
||||
if c.KubeClient.Get(ctx, types.NamespacedName{Namespace: kubevelatypes.DefaultKubeVelaNS, Name: cm.Name}, exist) == nil {
|
||||
if exist.Data == nil {
|
||||
cm.Data = map[string]string{}
|
||||
}
|
||||
exist.Data["config"] = string(bs)
|
||||
return c.KubeClient.Update(ctx, &cm)
|
||||
}
|
||||
return c.KubeClient.Create(ctx, &cm)
|
||||
}
|
||||
|
||||
func makeUserConfigName(userName string) string {
|
||||
return fmt.Sprintf("users-%s-kubeconfig", userName)
|
||||
}
|
||||
|
||||
func makeUserCloudShellName(userName string) string {
|
||||
return fmt.Sprintf("users-%s", userName)
|
||||
}
|
||||
|
||||
func (c *cloudShellServiceImpl) newCloudShell(ctx context.Context) (*v1alpha1.CloudShell, error) {
|
||||
var userName string
|
||||
if user := ctx.Value(&apisv1.CtxKeyUser); user != nil {
|
||||
if u, ok := user.(string); ok {
|
||||
userName = u
|
||||
}
|
||||
}
|
||||
if userName == "" {
|
||||
return nil, bcode.ErrUnauthorized
|
||||
}
|
||||
var cs v1alpha1.CloudShell
|
||||
cs.Name = fmt.Sprintf("users-%s", userName)
|
||||
cs.Namespace = kubevelatypes.DefaultKubeVelaNS
|
||||
cs.Labels = map[string]string{
|
||||
DefaultLabelKey: "cloudshell",
|
||||
}
|
||||
cs.Spec.ConfigmapName = makeUserConfigName(userName)
|
||||
cs.Spec.RunAsUser = userName
|
||||
// only one client and exit on disconnection
|
||||
once, _ := strconv.ParseBool(os.Getenv("CLOUDSHELL_ONCE"))
|
||||
cs.Spec.Once = once
|
||||
cs.Spec.Cleanup = true
|
||||
cs.Spec.CommandAction = DefaultCloudShellCommand
|
||||
cs.Spec.ExposeMode = v1alpha1.ExposureServiceClusterIP
|
||||
cs.Spec.PathPrefix = DefaultCloudShellPathPrefix
|
||||
return &cs, nil
|
||||
}
|
||||
|
||||
func checkReadOnly(projectName string, permissions []*model.Permission) bool {
|
||||
ra := &RequestResourceAction{}
|
||||
ra.SetResourceWithName("project:{projectName}/application", func(name string) string {
|
||||
return projectName
|
||||
})
|
||||
ra.SetActions([]string{"create"})
|
||||
return !ra.Match(permissions)
|
||||
}
|
||||
|
||||
// managePrivilegesForUser grant or revoke privileges for a user
|
||||
func (c *cloudShellServiceImpl) managePrivilegesForUser(ctx context.Context, projectName string, readOnly bool, userName string, revoke bool) error {
|
||||
|
||||
targets, err := c.TargetService.ListTargets(ctx, 0, 0, projectName)
|
||||
if err != nil {
|
||||
log.Logger.Infof("failed to list the targets by the project name %s :%s", projectName, err.Error())
|
||||
}
|
||||
var authPDs []auth.PrivilegeDescription
|
||||
for _, t := range targets.Targets {
|
||||
authPDs = append(authPDs, &auth.ScopedPrivilege{Cluster: t.Cluster.ClusterName, Namespace: t.Cluster.Namespace, ReadOnly: readOnly})
|
||||
}
|
||||
envs, err := c.EnvService.ListEnvs(ctx, 0, 0, apisv1.ListEnvOptions{Project: projectName})
|
||||
if err != nil {
|
||||
log.Logger.Infof("failed to list the envs by the project name %s :%s", projectName, err.Error())
|
||||
}
|
||||
for _, e := range envs.Envs {
|
||||
authPDs = append(authPDs, &auth.ApplicationPrivilege{Cluster: kubevelatypes.ClusterLocalName, Namespace: e.Namespace, ReadOnly: readOnly})
|
||||
}
|
||||
|
||||
identity := &auth.Identity{User: userName}
|
||||
writer := &bytes.Buffer{}
|
||||
f, msg := auth.GrantPrivileges, "GrantPrivileges"
|
||||
if revoke {
|
||||
f, msg = auth.RevokePrivileges, "RevokePrivileges"
|
||||
}
|
||||
if err := f(ctx, c.KubeClient, authPDs, identity, writer); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Logger.Debugf("%s: %s", msg, writer.String())
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
Copyright 2022 The KubeVela 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"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
v1alpha1 "github.com/cloudtty/cloudtty/pkg/apis/cloudshell/v1alpha1"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
kubevelatypes "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
)
|
||||
|
||||
var _ = Describe("Test cloudshell service function", func() {
|
||||
var (
|
||||
ds datastore.DataStore
|
||||
cloudShellService *cloudShellServiceImpl
|
||||
userService *userServiceImpl
|
||||
projectService *projectServiceImpl
|
||||
err error
|
||||
database string
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
database = "cloudshell-test-kubevela"
|
||||
ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: database})
|
||||
Expect(err).Should(Succeed())
|
||||
envService := &envServiceImpl{
|
||||
Store: ds,
|
||||
KubeClient: k8sClient,
|
||||
}
|
||||
userService = &userServiceImpl{
|
||||
Store: ds,
|
||||
SysService: &systemInfoServiceImpl{Store: ds},
|
||||
ProjectService: projectService,
|
||||
}
|
||||
projectService = &projectServiceImpl{
|
||||
Store: ds,
|
||||
RbacService: &rbacServiceImpl{
|
||||
Store: ds,
|
||||
},
|
||||
TargetService: &targetServiceImpl{
|
||||
Store: ds,
|
||||
K8sClient: k8sClient,
|
||||
},
|
||||
EnvService: envService,
|
||||
UserService: userService,
|
||||
}
|
||||
userService.ProjectService = projectService
|
||||
userService.RbacService = projectService.RbacService
|
||||
envService.ProjectService = projectService
|
||||
|
||||
cloudShellService = &cloudShellServiceImpl{
|
||||
KubeClient: k8sClient,
|
||||
KubeConfig: cfg,
|
||||
ProjectService: projectService,
|
||||
TargetService: &targetServiceImpl{
|
||||
Store: ds,
|
||||
K8sClient: k8sClient,
|
||||
},
|
||||
EnvService: envService,
|
||||
UserService: userService,
|
||||
RBACService: projectService.RbacService,
|
||||
GenerateKubeConfig: func(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, options ...auth.KubeConfigGenerateOption) (*clientcmdapi.Config, error) {
|
||||
return &clientcmdapi.Config{}, nil
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("test prepareKubeConfig", func() {
|
||||
err = userService.Init(context.TODO())
|
||||
Expect(err).Should(BeNil())
|
||||
err = projectService.Init(context.TODO())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("test the developer users")
|
||||
|
||||
_, err = userService.CreateUser(context.TODO(), apisv1.CreateUserRequest{Name: "test-dev", Password: "test"})
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
_, err = projectService.AddProjectUser(context.TODO(), "default", apisv1.AddProjectUserRequest{
|
||||
UserName: "test-dev",
|
||||
UserRoles: []string{"app-developer"},
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
permissions, err := projectService.RbacService.GetUserPermissions(context.TODO(), &model.User{Name: "test-dev"}, "default", false)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(checkReadOnly("default", permissions)).Should(BeFalse())
|
||||
|
||||
ctx := context.WithValue(context.TODO(), &apisv1.CtxKeyUser, "test-dev")
|
||||
|
||||
err = cloudShellService.prepareKubeConfig(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
var rb rbacv1.RoleBinding
|
||||
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: "kubevela:writer:application:binding", Namespace: "default"}, &rb)
|
||||
Expect(err).Should(BeNil())
|
||||
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: "kubevela:writer:binding", Namespace: "default"}, &rb)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("test the viewer users")
|
||||
|
||||
_, err = userService.CreateUser(context.TODO(), apisv1.CreateUserRequest{Name: "test-viewer", Password: "test"})
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
_, err = projectService.AddProjectUser(context.TODO(), "default", apisv1.AddProjectUserRequest{
|
||||
UserName: "test-viewer",
|
||||
UserRoles: []string{"project-viewer"},
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
permissions, err = projectService.RbacService.GetUserPermissions(ctx, &model.User{Name: "test-viewer"}, "default", false)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(checkReadOnly("default", permissions)).Should(BeTrue())
|
||||
|
||||
ctx = context.WithValue(context.TODO(), &apisv1.CtxKeyUser, "test-viewer")
|
||||
|
||||
err = cloudShellService.prepareKubeConfig(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: "kubevela:reader:application:binding", Namespace: "default"}, &rb)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(rb.Subjects)).Should(Equal(1))
|
||||
Expect(rb.Subjects[0].Name).Should(Equal("test-viewer"))
|
||||
Expect(rb.Subjects[0].Kind).Should(Equal("User"))
|
||||
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: "kubevela:reader:binding", Namespace: "default"}, &rb)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("test the administrator users")
|
||||
|
||||
_, err = userService.CreateUser(context.TODO(), apisv1.CreateUserRequest{Name: "admin-test", Password: "test", Roles: []string{"admin"}})
|
||||
Expect(err).Should(BeNil())
|
||||
ctx = context.WithValue(context.TODO(), &apisv1.CtxKeyUser, "admin-test")
|
||||
|
||||
err = cloudShellService.prepareKubeConfig(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
var cm corev1.ConfigMap
|
||||
err = k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: kubevelatypes.DefaultKubeVelaNS, Name: makeUserConfigName("admin-test")}, &cm)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(cm.Data["identity"]) > 0).Should(BeTrue())
|
||||
var identity auth.Identity
|
||||
err = yaml.Unmarshal([]byte(cm.Data["identity"]), &identity)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(utils.StringsContain(identity.Groups, utils.KubeVelaAdminGroupPrefix+"admin")).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("test prepare", func() {
|
||||
By("With not CRD")
|
||||
_, err = userService.CreateUser(context.TODO(), apisv1.CreateUserRequest{Name: "test", Password: "test"})
|
||||
Expect(err).Should(BeNil())
|
||||
ctx := context.WithValue(context.TODO(), &apisv1.CtxKeyUser, "test")
|
||||
_, err := cloudShellService.Prepare(ctx)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
Expect(err.Error()).Should(Equal(bcode.ErrCloudShellAddonNotEnabled.Error()))
|
||||
|
||||
cloudshellCRDBytes, err := ioutil.ReadFile("./testdata/cloudshell-crd.yaml")
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
crd := apiextensionsv1.CustomResourceDefinition{}
|
||||
Expect(yaml.Unmarshal(cloudshellCRDBytes, &crd)).Should(BeNil())
|
||||
Expect(k8sClient.Create(context.TODO(), &crd)).Should(BeNil())
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
re, err := cloudShellService.Prepare(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(re.Status).Should(Equal(StatusPreparing))
|
||||
|
||||
var cloudShell v1alpha1.CloudShell
|
||||
err = k8sClient.Get(ctx, types.NamespacedName{Namespace: kubevelatypes.DefaultKubeVelaNS, Name: makeUserCloudShellName("test")}, &cloudShell)
|
||||
Expect(err).Should(BeNil())
|
||||
cloudShell.Status.Phase = v1alpha1.PhaseReady
|
||||
cloudShell.Status.AccessURL = "10.10.1.1:8765"
|
||||
err = k8sClient.Status().Update(context.Background(), &cloudShell)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
re, err = cloudShellService.Prepare(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(re.Status).Should(Equal(StatusCompleted))
|
||||
|
||||
endpoint, err := cloudShellService.GetCloudShellEndpoint(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(endpoint).Should(Equal("10.10.1.1:8765"))
|
||||
})
|
||||
})
|
||||
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
|
|
@ -25,12 +26,15 @@ import (
|
|||
apierror "k8s.io/apimachinery/pkg/api/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/repository"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
util "github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
|
@ -89,6 +93,11 @@ func (p *envServiceImpl) DeleteEnv(ctx context.Context, envName string) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := managePrivilegesForEnvironment(ctx, p.KubeClient, env, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +254,10 @@ func (p *envServiceImpl) UpdateEnv(ctx context.Context, name string, req apisv1.
|
|||
}
|
||||
}
|
||||
|
||||
if err := managePrivilegesForEnvironment(ctx, p.KubeClient, env, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := convertEnvModel2Base(env, targets)
|
||||
return resp, nil
|
||||
}
|
||||
|
|
@ -286,6 +299,10 @@ func (p *envServiceImpl) CreateEnv(ctx context.Context, req apisv1.CreateEnvRequ
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := managePrivilegesForEnvironment(ctx, p.KubeClient, newEnv, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := convertEnvModel2Base(newEnv, targets)
|
||||
return resp, nil
|
||||
}
|
||||
|
|
@ -336,3 +353,24 @@ func convertEnvModel2Base(env *model.Env, targets []*model.Target) *apisv1.Env {
|
|||
}
|
||||
return &data
|
||||
}
|
||||
|
||||
// managePrivilegesForEnvironment grant or revoke privileges for environment
|
||||
func managePrivilegesForEnvironment(ctx context.Context, cli client.Client, env *model.Env, revoke bool) error {
|
||||
p := &auth.ApplicationPrivilege{Cluster: types.ClusterLocalName, Namespace: env.Namespace}
|
||||
identity := &auth.Identity{Groups: []string{utils.KubeVelaProjectGroupPrefix + env.Project}}
|
||||
writer := &bytes.Buffer{}
|
||||
f, msg := auth.GrantPrivileges, "GrantPrivileges"
|
||||
if revoke {
|
||||
f, msg = auth.RevokePrivileges, "RevokePrivileges"
|
||||
}
|
||||
if err := f(ctx, cli, []auth.PrivilegeDescription{p}, identity, writer); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Logger.Debugf("%s: %s", msg, writer.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewTestEnvService create the env service instance for testing
|
||||
func NewTestEnvService(ds datastore.DataStore, c client.Client) EnvService {
|
||||
return &envServiceImpl{Store: ds, KubeClient: c, ProjectService: NewTestProjectService(ds, c)}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ import (
|
|||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
|
|
@ -74,23 +76,28 @@ var _ = Describe("Test env service functions", func() {
|
|||
req3 := apisv1.CreateEnvRequest{
|
||||
Name: "test-env-2",
|
||||
Description: "this is a env description",
|
||||
Namespace: "default",
|
||||
Namespace: "test-env-22",
|
||||
Project: "env-project",
|
||||
Targets: []string{"env-test"},
|
||||
}
|
||||
base, err = envService.CreateEnv(context.TODO(), req3)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(base.Namespace, "default")).Should(BeEmpty())
|
||||
Expect(cmp.Diff(base.Namespace, req3.Namespace)).Should(BeEmpty())
|
||||
var namespace corev1.Namespace
|
||||
err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: base.Namespace}, &namespace)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(namespace.Labels[oam.LabelNamespaceOfEnvName], req3.Name)).Should(BeEmpty())
|
||||
|
||||
var roleBinding rbacv1.RoleBinding
|
||||
err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: auth.KubeVelaWriterAppRoleName + ":binding", Namespace: base.Namespace}, &roleBinding)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(roleBinding.RoleRef.Name, auth.KubeVelaWriterAppRoleName)).Should(BeEmpty())
|
||||
|
||||
// test env target conflict
|
||||
req4 := apisv1.CreateEnvRequest{
|
||||
Name: "test-env-3",
|
||||
Description: "this is a env description",
|
||||
Namespace: "default",
|
||||
Namespace: "test-env-22",
|
||||
Project: "env-project",
|
||||
Targets: []string{"env-test"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,9 @@ func (i *imageImpl) GetImageInfo(ctx context.Context, project, secretName, image
|
|||
if secret.Labels[types.LabelConfigProject] == "" || secret.Labels[types.LabelConfigProject] == project {
|
||||
if secretName == secret.Name {
|
||||
selectSecret = append(selectSecret, &secrets.Items[i])
|
||||
selectSecretNames = append(selectSecretNames, secret.Name)
|
||||
if secret.Type == corev1.SecretTypeDockerConfigJson {
|
||||
selectSecretNames = append(selectSecretNames, secret.Name)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +122,9 @@ func (i *imageImpl) GetImageInfo(ctx context.Context, project, secretName, image
|
|||
if secret.Labels[types.LabelConfigProject] == "" || secret.Labels[types.LabelConfigProject] == project {
|
||||
if secret.Labels[types.LabelConfigIdentifier] == registryDomain {
|
||||
selectSecret = append(selectSecret, &secrets.Items[i])
|
||||
selectSecretNames = append(selectSecretNames, secret.Name)
|
||||
if secret.Type == corev1.SecretTypeDockerConfigJson {
|
||||
selectSecretNames = append(selectSecretNames, secret.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import (
|
|||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/repository"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
|
|
@ -58,9 +57,12 @@ type ProjectService interface {
|
|||
}
|
||||
|
||||
type projectServiceImpl struct {
|
||||
Store datastore.DataStore `inject:"datastore"`
|
||||
K8sClient client.Client `inject:"kubeClient"`
|
||||
RbacService RBACService `inject:""`
|
||||
Store datastore.DataStore `inject:"datastore"`
|
||||
K8sClient client.Client `inject:"kubeClient"`
|
||||
RbacService RBACService `inject:""`
|
||||
TargetService TargetService `inject:""`
|
||||
UserService UserService `inject:""`
|
||||
EnvService EnvService `inject:""`
|
||||
}
|
||||
|
||||
// NewProjectService new project service
|
||||
|
|
@ -111,26 +113,35 @@ func (p *projectServiceImpl) InitDefaultProjectEnvTarget(ctx context.Context, de
|
|||
}
|
||||
log.Logger.Info("no default project found, adding a default project with default env and target")
|
||||
|
||||
if err := repository.CreateTargetNamespace(ctx, p.K8sClient, multicluster.ClusterLocalName, defaultNamespace, model.DefaultInitName); err != nil {
|
||||
return fmt.Errorf("initialize default target namespace failed %w", err)
|
||||
_, err = p.CreateProject(ctx, apisv1.CreateProjectRequest{
|
||||
Name: model.DefaultInitName,
|
||||
Alias: "Default",
|
||||
Description: model.DefaultProjectDescription,
|
||||
Owner: model.DefaultAdminUserName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize project failed %w", err)
|
||||
}
|
||||
|
||||
// initialize default target first
|
||||
err = repository.CreateTarget(ctx, p.Store, &model.Target{
|
||||
_, err = p.TargetService.CreateTarget(ctx, apisv1.CreateTargetRequest{
|
||||
Name: model.DefaultInitName,
|
||||
Alias: "Default",
|
||||
Description: model.DefaultTargetDescription,
|
||||
Cluster: &model.ClusterTarget{
|
||||
Project: model.DefaultInitName,
|
||||
Cluster: &apisv1.ClusterTarget{
|
||||
ClusterName: multicluster.ClusterLocalName,
|
||||
Namespace: defaultNamespace,
|
||||
},
|
||||
})
|
||||
|
||||
// for idempotence, ignore default target already exist error
|
||||
if err != nil && errors.Is(err, bcode.ErrTargetExist) {
|
||||
return fmt.Errorf("initialize default target failed %w", err)
|
||||
}
|
||||
|
||||
// initialize default target first
|
||||
err = repository.CreateEnv(ctx, p.K8sClient, p.Store, &model.Env{
|
||||
_, err = p.EnvService.CreateEnv(ctx, apisv1.CreateEnvRequest{
|
||||
Name: model.DefaultInitName,
|
||||
Alias: "Default",
|
||||
Description: model.DefaultEnvDescription,
|
||||
|
|
@ -142,16 +153,6 @@ func (p *projectServiceImpl) InitDefaultProjectEnvTarget(ctx context.Context, de
|
|||
if err != nil && errors.Is(err, bcode.ErrEnvAlreadyExists) {
|
||||
return fmt.Errorf("initialize default environment failed %w", err)
|
||||
}
|
||||
|
||||
_, err = p.CreateProject(ctx, apisv1.CreateProjectRequest{
|
||||
Name: model.DefaultInitName,
|
||||
Alias: "Default",
|
||||
Description: model.DefaultProjectDescription,
|
||||
Owner: model.DefaultAdminUserName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize project failed %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -399,6 +400,10 @@ func (p *projectServiceImpl) AddProjectUser(ctx context.Context, projectName str
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = p.UserService.GetUser(ctx, req.UserName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// check user roles
|
||||
for _, role := range req.UserRoles {
|
||||
var projectUser = model.Role{
|
||||
|
|
@ -618,3 +623,22 @@ func retrieveConfigFromApplication(a v1beta1.Application, project string) *apisv
|
|||
Description: a.Annotations[types.AnnotationConfigDescription],
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestProjectService create the project service instance for testing
|
||||
func NewTestProjectService(ds datastore.DataStore, c client.Client) ProjectService {
|
||||
targetImpl := &targetServiceImpl{K8sClient: c, Store: ds}
|
||||
envImpl := &envServiceImpl{KubeClient: c, Store: ds}
|
||||
rbacService := &rbacServiceImpl{Store: ds}
|
||||
userService := &userServiceImpl{Store: ds, RbacService: rbacService, SysService: systemInfoServiceImpl{Store: ds}}
|
||||
projectService := &projectServiceImpl{
|
||||
K8sClient: c,
|
||||
Store: ds,
|
||||
RbacService: rbacService,
|
||||
TargetService: targetImpl,
|
||||
UserService: userService,
|
||||
EnvService: envImpl,
|
||||
}
|
||||
userService.ProjectService = projectService
|
||||
envImpl.ProjectService = projectService
|
||||
return projectService
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,16 +56,17 @@ var _ = Describe("Test project service functions", func() {
|
|||
ns.Name = defaultNamespace
|
||||
err = k8sClient.Create(context.TODO(), &ns)
|
||||
Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
projectService = NewTestProjectService(ds, k8sClient).(*projectServiceImpl)
|
||||
envImpl = projectService.EnvService.(*envServiceImpl)
|
||||
userService = projectService.UserService.(*userServiceImpl)
|
||||
targetImpl = projectService.TargetService.(*targetServiceImpl)
|
||||
|
||||
projectService = &projectServiceImpl{K8sClient: k8sClient, Store: ds, RbacService: &rbacServiceImpl{Store: ds}}
|
||||
pp, err := projectService.ListProjects(context.TODO(), 0, 0)
|
||||
Expect(err).Should(BeNil())
|
||||
// reset all projects
|
||||
for _, p := range pp.Projects {
|
||||
_ = projectService.DeleteProject(context.TODO(), p.Name)
|
||||
}
|
||||
|
||||
envImpl = &envServiceImpl{KubeClient: k8sClient, Store: ds, ProjectService: projectService}
|
||||
ctx := context.WithValue(context.TODO(), &apisv1.CtxKeyUser, "admin")
|
||||
envs, err := envImpl.ListEnvs(ctx, 0, 0, apisv1.ListEnvOptions{})
|
||||
Expect(err).Should(BeNil())
|
||||
|
|
@ -73,7 +74,6 @@ var _ = Describe("Test project service functions", func() {
|
|||
for _, e := range envs.Envs {
|
||||
_ = envImpl.DeleteEnv(context.TODO(), e.Name)
|
||||
}
|
||||
targetImpl = &targetServiceImpl{K8sClient: k8sClient, Store: ds}
|
||||
targets, err := targetImpl.ListTargets(context.TODO(), 0, 0, "")
|
||||
Expect(err).Should(BeNil())
|
||||
// reset all projects
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -25,7 +26,9 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/repository"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
|
|
@ -34,6 +37,7 @@ import (
|
|||
apiserverutils "github.com/oam-dev/kubevela/pkg/apiserver/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
|
|
@ -44,12 +48,20 @@ var reg = regexp.MustCompile(`(?U)\{.*\}`)
|
|||
|
||||
var defaultProjectPermissionTemplate = []*model.PermissionTemplate{
|
||||
{
|
||||
Name: "project-read",
|
||||
Alias: "Project Read",
|
||||
Resources: []string{"project:{projectName}"},
|
||||
Actions: []string{"detail"},
|
||||
Effect: "Allow",
|
||||
Scope: "project",
|
||||
Name: "project-view",
|
||||
Alias: "Project View",
|
||||
Resources: []string{
|
||||
"project:{projectName}",
|
||||
"project:{projectName}/config:*",
|
||||
"project:{projectName}/role:*",
|
||||
"project:{projectName}/projectUser:*",
|
||||
"project:{projectName}/permission:*",
|
||||
"project:{projectName}/environment:*",
|
||||
"project:{projectName}/application:*/*",
|
||||
},
|
||||
Actions: []string{"detail", "list"},
|
||||
Effect: "Allow",
|
||||
Scope: "project",
|
||||
},
|
||||
{
|
||||
Name: "app-management",
|
||||
|
|
@ -86,6 +98,14 @@ var defaultProjectPermissionTemplate = []*model.PermissionTemplate{
|
|||
}
|
||||
|
||||
var defaultPlatformPermission = []*model.PermissionTemplate{
|
||||
{
|
||||
Name: "disable-cloudshell",
|
||||
Alias: "Disable CloudShell",
|
||||
Resources: []string{"cloudshell"},
|
||||
Actions: []string{"*"},
|
||||
Effect: "Deny",
|
||||
Scope: "platform",
|
||||
},
|
||||
{
|
||||
Name: "cluster-management",
|
||||
Alias: "Cluster Management",
|
||||
|
|
@ -102,6 +122,14 @@ var defaultPlatformPermission = []*model.PermissionTemplate{
|
|||
Effect: "Allow",
|
||||
Scope: "platform",
|
||||
},
|
||||
{
|
||||
Name: "project-list",
|
||||
Alias: "Project List",
|
||||
Resources: []string{"project:*"},
|
||||
Actions: []string{"list"},
|
||||
Effect: "Allow",
|
||||
Scope: "platform",
|
||||
},
|
||||
{
|
||||
Name: "addon-management",
|
||||
Alias: "Addon Management",
|
||||
|
|
@ -239,6 +267,7 @@ var ResourceMaps = map[string]resourceMetadata{
|
|||
},
|
||||
},
|
||||
},
|
||||
"cloudshell": {},
|
||||
}
|
||||
|
||||
var existResourcePaths = convert(ResourceMaps)
|
||||
|
|
@ -332,7 +361,8 @@ func registerResourceAction(resource string, actions ...string) {
|
|||
}
|
||||
|
||||
type rbacServiceImpl struct {
|
||||
Store datastore.DataStore `inject:"datastore"`
|
||||
Store datastore.DataStore `inject:"datastore"`
|
||||
KubeClient client.Client `inject:"kubeClient"`
|
||||
}
|
||||
|
||||
// RBACService implement RBAC-related business logic.
|
||||
|
|
@ -365,26 +395,29 @@ func (p *rbacServiceImpl) Init(ctx context.Context) error {
|
|||
},
|
||||
},
|
||||
})
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
var batchData []datastore.Entity
|
||||
for _, policy := range defaultPlatformPermission {
|
||||
batchData = append(batchData, &model.Permission{
|
||||
Name: policy.Name,
|
||||
Alias: policy.Alias,
|
||||
Resources: policy.Resources,
|
||||
Actions: policy.Actions,
|
||||
Effect: policy.Effect,
|
||||
if count == 0 {
|
||||
var batchData []datastore.Entity
|
||||
for _, policy := range defaultPlatformPermission {
|
||||
batchData = append(batchData, &model.Permission{
|
||||
Name: policy.Name,
|
||||
Alias: policy.Alias,
|
||||
Resources: policy.Resources,
|
||||
Actions: policy.Actions,
|
||||
Effect: policy.Effect,
|
||||
})
|
||||
}
|
||||
batchData = append(batchData, &model.Role{
|
||||
Name: "admin",
|
||||
Alias: "Admin",
|
||||
Permissions: []string{"admin"},
|
||||
})
|
||||
if err := p.Store.BatchAdd(ctx, batchData); err != nil {
|
||||
return fmt.Errorf("init the platform perm policies failure %w", err)
|
||||
}
|
||||
}
|
||||
batchData = append(batchData, &model.Role{
|
||||
Name: "admin",
|
||||
Alias: "Admin",
|
||||
Permissions: []string{"admin"},
|
||||
})
|
||||
if err := p.Store.BatchAdd(ctx, batchData); err != nil {
|
||||
return fmt.Errorf("init the platform perm policies failure %w", err)
|
||||
|
||||
if err := managePrivilegesForAdminUser(ctx, p.KubeClient, "admin", false); err != nil {
|
||||
return fmt.Errorf("failed to init the RBAC in cluster for the admin role %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -447,6 +480,13 @@ func (p *rbacServiceImpl) GetUserPermissions(ctx context.Context, user *model.Us
|
|||
perms = append(perms, projectPerms...)
|
||||
}
|
||||
}
|
||||
// with the default permissions
|
||||
perms = append(perms, &model.Permission{
|
||||
Name: "cloudshell",
|
||||
Resources: []string{"cloudshell"},
|
||||
Actions: []string{"create"},
|
||||
Effect: "Allow",
|
||||
})
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
|
|
@ -822,6 +862,11 @@ func (p *rbacServiceImpl) InitDefaultRoleAndUsersForProject(ctx context.Context,
|
|||
Alias: "Project Admin",
|
||||
Permissions: []string{"project-read", "app-management", "env-management", "role-management", "configuration-read"},
|
||||
Project: project.Name,
|
||||
}, &model.Role{
|
||||
Name: "project-viewer",
|
||||
Alias: "Project Viewer",
|
||||
Permissions: []string{"project-view"},
|
||||
Project: project.Name,
|
||||
})
|
||||
if project.Owner != "" {
|
||||
var projectUser = &model.ProjectUser{
|
||||
|
|
@ -964,3 +1009,19 @@ func (r *RequestResourceAction) Match(policies []*model.Permission) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// managePrivilegesForAdminUser grant or revoke privileges for admin user
|
||||
func managePrivilegesForAdminUser(ctx context.Context, cli client.Client, roleName string, revoke bool) error {
|
||||
p := &auth.ScopedPrivilege{Cluster: types.ClusterLocalName}
|
||||
identity := &auth.Identity{Groups: []string{apiserverutils.KubeVelaAdminGroupPrefix + roleName}}
|
||||
writer := &bytes.Buffer{}
|
||||
f, msg := auth.GrantPrivileges, "GrantPrivileges"
|
||||
if revoke {
|
||||
f, msg = auth.RevokePrivileges, "RevokePrivileges"
|
||||
}
|
||||
if err := f(ctx, cli, []auth.PrivilegeDescription{p}, identity, writer); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Logger.Debugf("%s: %s", msg, writer.String())
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,12 +93,12 @@ var _ = Describe("Test rbac service", func() {
|
|||
})
|
||||
|
||||
It("Test init and list platform permissions", func() {
|
||||
rbacService := rbacServiceImpl{Store: ds}
|
||||
rbacService := rbacServiceImpl{Store: ds, KubeClient: k8sClient}
|
||||
err := rbacService.Init(context.TODO())
|
||||
Expect(err).Should(BeNil())
|
||||
policies, err := rbacService.ListPermissions(context.TODO(), "")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(policies)).Should(BeEquivalentTo(int64(8)))
|
||||
Expect(len(policies)).Should(BeEquivalentTo(int64(10)))
|
||||
})
|
||||
|
||||
It("Test checkPerm by admin user", func() {
|
||||
|
|
@ -194,7 +194,7 @@ var _ = Describe("Test rbac service", func() {
|
|||
|
||||
roles, err := rbacService.ListRole(context.TODO(), "init-test", 0, 0)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(roles.Total).Should(BeEquivalentTo(int64(2)))
|
||||
Expect(roles.Total).Should(BeEquivalentTo(int64(3)))
|
||||
|
||||
policies, err := rbacService.ListPermissions(context.TODO(), "init-test")
|
||||
Expect(err).Should(BeNil())
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func InitServiceBean(c config.Config) []interface{} {
|
|||
return []interface{}{
|
||||
clusterService, rbacService, projectService, envService, targetService, workflowService, oamApplicationService,
|
||||
velaQLService, definitionService, addonService, envBindingService, systemInfoService, helmService, userService,
|
||||
authenticationService, configService, applicationService, webhookService, NewImageService(),
|
||||
authenticationService, configService, applicationService, webhookService, NewImageService(), NewCloudShellService(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ func (dt *targetServiceImpl) Init(ctx context.Context) error {
|
|||
if err := dt.Store.Put(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := managePrivilegesForTarget(ctx, dt.K8sClient, t, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -163,6 +166,10 @@ func (dt *targetServiceImpl) UpdateTarget(ctx context.Context, target *model.Tar
|
|||
if err := dt.Store.Put(ctx, targetModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Compatible with historical data, if the existing Target has not been authorized, perform an update action.
|
||||
if err := managePrivilegesForTarget(ctx, dt.K8sClient, targetModel, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dt.DetailTarget(ctx, targetModel)
|
||||
}
|
||||
|
||||
|
|
@ -255,3 +262,8 @@ func managePrivilegesForTarget(ctx context.Context, cli client.Client, target *m
|
|||
log.Logger.Debugf("%s: %s", msg, writer.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewTestTargetService create the target service instance for testing
|
||||
func NewTestTargetService(ds datastore.DataStore, c client.Client) TargetService {
|
||||
return &targetServiceImpl{Store: ds, K8sClient: c}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.8.0
|
||||
creationTimestamp: null
|
||||
name: cloudshells.cloudshell.cloudtty.io
|
||||
spec:
|
||||
group: cloudshell.cloudtty.io
|
||||
names:
|
||||
kind: CloudShell
|
||||
listKind: CloudShellList
|
||||
plural: cloudshells
|
||||
singular: cloudshell
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: User
|
||||
jsonPath: .spec.runAsUser
|
||||
name: User
|
||||
type: string
|
||||
- description: Command
|
||||
jsonPath: .spec.commandAction
|
||||
name: Command
|
||||
type: string
|
||||
- description: Expose mode
|
||||
jsonPath: .spec.exposureMode
|
||||
name: Type
|
||||
type: string
|
||||
- description: Access Url
|
||||
jsonPath: .status.accessUrl
|
||||
name: URL
|
||||
type: string
|
||||
- description: Phase
|
||||
jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: Age
|
||||
type: date
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: CloudShell is the Schema for the cloudshells 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: CloudShellSpec defines the desired state of CloudShell
|
||||
properties:
|
||||
cleanup:
|
||||
description: Cleanup specified whether to delete cloudshell resources
|
||||
when corresponding job status is completed.
|
||||
type: boolean
|
||||
commandAction:
|
||||
type: string
|
||||
configmapName:
|
||||
description: Configmap of the target kube-config, will replace by
|
||||
SA
|
||||
type: string
|
||||
exposureMode:
|
||||
description: ExposeMode describes how to access ttyd service, either
|
||||
ClusterIP, NodePort, Ingress or VirtualService.
|
||||
enum:
|
||||
- ClusterIP
|
||||
- NodePort
|
||||
- Ingress
|
||||
- VirtualService
|
||||
type: string
|
||||
ingressConfig:
|
||||
description: Specifies a port number range 30000-32767 when using
|
||||
nodeport mode, if not specified, kubernetes default random rule
|
||||
is used. NodePort int32 `json:"NodePort,omitempty"` IngressConfig
|
||||
specifies necessary parameters to create ingress.
|
||||
properties:
|
||||
ingressClassName:
|
||||
description: IngressClassName specifies a ingress controller to
|
||||
ingress, it must be fill when the cluster have multiple ingress
|
||||
controller service.
|
||||
type: string
|
||||
ingressName:
|
||||
description: IngressName specifies a name to ingress, if it's
|
||||
empty, default "cloudshell-ingress".
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace specifies a namespace that the virtualService
|
||||
will be created in it. if it's empty, default the cloudshell
|
||||
namespace.
|
||||
type: string
|
||||
type: object
|
||||
once:
|
||||
description: accept only one client and exit on disconnection
|
||||
type: boolean
|
||||
pathPrefix:
|
||||
description: PathPrefix specified a path prefix to access url, if
|
||||
not, the default path is used.
|
||||
type: string
|
||||
runAsUser:
|
||||
type: string
|
||||
ttl:
|
||||
format: int32
|
||||
type: integer
|
||||
virtualServiceConfig:
|
||||
description: VirtualServiceConfig specifies some of the parameters
|
||||
necessary to create the virtaulService.
|
||||
properties:
|
||||
export_to:
|
||||
description: The value "." is reserved and defines an export to
|
||||
the same namespace that the virtual service is declared in.
|
||||
Similarly the value "*" is reserved and defines an export to
|
||||
all namespaces.
|
||||
type: string
|
||||
gateway:
|
||||
description: Gateway must be specified and the gateway already
|
||||
exists in the cluster.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace specifies a namespace that the virtualService
|
||||
will be created in it. if it's empty, default the cloudshell
|
||||
namespace.
|
||||
type: string
|
||||
virtualServiceName:
|
||||
description: VirtualServiceName specifies a name to virtualService,
|
||||
if it's empty, default "cloudshell-VirtualService"
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
status:
|
||||
description: CloudShellStatus defines the observed state of CloudShell
|
||||
properties:
|
||||
accessUrl:
|
||||
type: string
|
||||
phase:
|
||||
description: 'INSERT ADDITIONAL STATUS FIELD - define observed state
|
||||
of cluster Important: Run "make" to regenerate code after modifying
|
||||
this file'
|
||||
type: string
|
||||
required:
|
||||
- accessUrl
|
||||
- phase
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
|
|
@ -170,6 +170,7 @@ func (u *userServiceImpl) CreateUser(ctx context.Context, req apisv1.CreateUserR
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: validate the roles, they must be platform roles
|
||||
user := &model.User{
|
||||
Name: req.Name,
|
||||
|
|
@ -415,3 +416,13 @@ func compareHashWithPassword(hash, password string) error {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// NewTestUserService create the user service instance for testing
|
||||
func NewTestUserService(ds datastore.DataStore, c client.Client) UserService {
|
||||
return &userServiceImpl{
|
||||
Store: ds, K8sClient: c,
|
||||
ProjectService: NewTestProjectService(ds, c),
|
||||
RbacService: &rbacServiceImpl{Store: ds},
|
||||
SysService: &systemInfoServiceImpl{Store: ds, KubeClient: c},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ type CR2UX struct {
|
|||
cache sync.Map
|
||||
projectService service.ProjectService
|
||||
applicationService service.ApplicationService
|
||||
targetService service.TargetService
|
||||
envService service.EnvService
|
||||
}
|
||||
|
||||
func formatAppComposedName(name, namespace string) string {
|
||||
|
|
@ -114,12 +116,12 @@ func (c *CR2UX) AddOrUpdate(ctx context.Context, targetApp *v1beta1.Application)
|
|||
return err
|
||||
}
|
||||
|
||||
if err = StoreTargets(ctx, dsApp, ds); err != nil {
|
||||
if err = StoreTargets(ctx, dsApp, ds, c.targetService); err != nil {
|
||||
log.Logger.Errorf("Store targets to data store err %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = StoreEnv(ctx, dsApp, ds); err != nil {
|
||||
if err = StoreEnv(ctx, dsApp, ds, c.envService); err != nil {
|
||||
log.Logger.Errorf("Store Env Metadata to data store err %v", err)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/service"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
|
|
@ -59,7 +60,7 @@ var _ = Describe("Test CR convert to ux", func() {
|
|||
|
||||
By("no app created, test the name")
|
||||
|
||||
cr2ux := CR2UX{ds: ds, cli: k8sClient, cache: sync.Map{}}
|
||||
cr2ux := newCR2UX(ds)
|
||||
gotApp, gotAppName, err := cr2ux.getApp(context.Background(), apName1, appNS1)
|
||||
Expect(gotAppName).Should(BeEquivalentTo(apName1))
|
||||
Expect(gotApp).Should(BeNil())
|
||||
|
|
@ -107,11 +108,7 @@ var _ = Describe("Test CR convert to ux", func() {
|
|||
err = k8sClient.Create(context.TODO(), &ns)
|
||||
Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
cr2ux := CR2UX{
|
||||
ds: ds,
|
||||
cli: k8sClient,
|
||||
cache: sync.Map{},
|
||||
}
|
||||
cr2ux := newCR2UX(ds)
|
||||
|
||||
By("create test app1 and check the syncing results")
|
||||
app1 := &v1beta1.Application{}
|
||||
|
|
@ -154,3 +151,22 @@ var _ = Describe("Test CR convert to ux", func() {
|
|||
})
|
||||
|
||||
})
|
||||
|
||||
func newCR2UX(ds datastore.DataStore) *CR2UX {
|
||||
projectService := service.NewTestProjectService(ds, k8sClient)
|
||||
applicationService := service.NewTestApplicationService(ds, k8sClient, cfg)
|
||||
targetService := service.NewTestTargetService(ds, k8sClient)
|
||||
envService := service.NewTestEnvService(ds, k8sClient)
|
||||
userService := service.NewTestUserService(ds, k8sClient)
|
||||
err := userService.Init(context.TODO())
|
||||
Expect(err).Should(BeNil())
|
||||
return &CR2UX{
|
||||
ds: ds,
|
||||
cli: k8sClient,
|
||||
cache: sync.Map{},
|
||||
projectService: projectService,
|
||||
targetService: targetService,
|
||||
envService: envService,
|
||||
applicationService: applicationService,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/service"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
v1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
|
@ -66,7 +67,7 @@ func StoreAppMeta(ctx context.Context, app *model.DataStoreApp, ds datastore.Dat
|
|||
}
|
||||
|
||||
// StoreEnv will sync application namespace from CR to datastore env, one namespace belongs to one env
|
||||
func StoreEnv(ctx context.Context, app *model.DataStoreApp, ds datastore.DataStore) error {
|
||||
func StoreEnv(ctx context.Context, app *model.DataStoreApp, ds datastore.DataStore, envService service.EnvService) error {
|
||||
curEnv := &model.Env{Name: app.Env.Name}
|
||||
err := ds.Get(ctx, curEnv)
|
||||
if err == nil {
|
||||
|
|
@ -80,7 +81,18 @@ func StoreEnv(ctx context.Context, app *model.DataStoreApp, ds datastore.DataSto
|
|||
// other database error, return it
|
||||
return err
|
||||
}
|
||||
return ds.Add(ctx, app.Env)
|
||||
_, err = envService.CreateEnv(ctx, v1.CreateEnvRequest{
|
||||
Name: app.Env.Name,
|
||||
Alias: app.Env.Alias,
|
||||
Description: app.Env.Description,
|
||||
Project: app.Env.Project,
|
||||
Namespace: app.Env.Namespace,
|
||||
Targets: app.Env.Targets,
|
||||
})
|
||||
if err != nil && !errors.Is(err, bcode.ErrEnvAlreadyExists) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StoreEnvBinding will add envbinding for application CR one application one envbinding
|
||||
|
|
@ -221,7 +233,7 @@ func StoreWorkflow(ctx context.Context, dsApp *model.DataStoreApp, ds datastore.
|
|||
}
|
||||
|
||||
// StoreTargets will sync targets from application CR to datastore
|
||||
func StoreTargets(ctx context.Context, dsApp *model.DataStoreApp, ds datastore.DataStore) error {
|
||||
func StoreTargets(ctx context.Context, dsApp *model.DataStoreApp, ds datastore.DataStore, targetService service.TargetService) error {
|
||||
for _, t := range dsApp.Targets {
|
||||
err := ds.Get(ctx, t)
|
||||
if err == nil {
|
||||
|
|
@ -231,7 +243,15 @@ func StoreTargets(ctx context.Context, dsApp *model.DataStoreApp, ds datastore.D
|
|||
// other database error, return it
|
||||
return err
|
||||
}
|
||||
if err = ds.Add(ctx, t); err != nil {
|
||||
_, err = targetService.CreateTarget(ctx, v1.CreateTargetRequest{
|
||||
Name: t.Name,
|
||||
Alias: t.Alias,
|
||||
Project: t.Project,
|
||||
Description: t.Description,
|
||||
Cluster: (*v1.ClusterTarget)(t.Cluster),
|
||||
Variable: t.Variable,
|
||||
})
|
||||
if err != nil && !errors.Is(err, bcode.ErrTargetExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,6 @@ var _ = BeforeSuite(func(done Done) {
|
|||
Expect(k8sClient).ToNot(BeNil())
|
||||
clients.SetKubeClient(k8sClient)
|
||||
By("new kube client success")
|
||||
clients.SetKubeClient(k8sClient)
|
||||
Expect(err).Should(BeNil())
|
||||
close(done)
|
||||
}, 240)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ type ApplicationSync struct {
|
|||
Store datastore.DataStore `inject:"datastore"`
|
||||
ProjectService service.ProjectService `inject:""`
|
||||
ApplicationService service.ApplicationService `inject:""`
|
||||
TargetService service.TargetService `inject:""`
|
||||
EnvService service.EnvService `inject:""`
|
||||
Queue workqueue.Interface
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +74,8 @@ func (a *ApplicationSync) Start(ctx context.Context, errorChan chan error) {
|
|||
cache: sync.Map{},
|
||||
projectService: a.ProjectService,
|
||||
applicationService: a.ApplicationService,
|
||||
targetService: a.TargetService,
|
||||
envService: a.EnvService,
|
||||
}
|
||||
if err = cu.initCache(ctx); err != nil {
|
||||
errorChan <- err
|
||||
|
|
|
|||
|
|
@ -62,11 +62,16 @@ var _ = Describe("Test Worker CR sync to datastore", func() {
|
|||
By("Start syncing")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
crux := newCR2UX(ds)
|
||||
appSync := &ApplicationSync{
|
||||
KubeClient: k8sClient,
|
||||
KubeConfig: cfg,
|
||||
Store: ds,
|
||||
Queue: workqueue.New(),
|
||||
KubeClient: k8sClient,
|
||||
KubeConfig: cfg,
|
||||
Store: ds,
|
||||
Queue: workqueue.New(),
|
||||
ProjectService: crux.projectService,
|
||||
ApplicationService: crux.applicationService,
|
||||
TargetService: crux.targetService,
|
||||
EnvService: crux.envService,
|
||||
}
|
||||
go appSync.Start(ctx, make(chan error))
|
||||
|
||||
|
|
|
|||
|
|
@ -286,7 +286,6 @@ func (s *enabledAddonAPIInterface) GetWebServiceRoute() *restful.WebService {
|
|||
ws.Route(ws.GET("/").To(s.list).
|
||||
Doc("list all enabled addons").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(s.RbacService.CheckPerm("addon", "list")).
|
||||
Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")).
|
||||
Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")).
|
||||
Returns(200, "OK", apis.ListEnabledAddonResponse{}).
|
||||
|
|
|
|||
|
|
@ -87,18 +87,27 @@ func (c *authenticationAPIInterface) GetWebServiceRoute() *restful.WebService {
|
|||
}
|
||||
|
||||
func authCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) {
|
||||
// support getting the token from the cookie
|
||||
var tokenValue string
|
||||
tokenHeader := req.HeaderParameter("Authorization")
|
||||
if tokenHeader == "" {
|
||||
bcode.ReturnError(req, res, bcode.ErrNotAuthorized)
|
||||
return
|
||||
if tokenHeader != "" {
|
||||
splitted := strings.Split(tokenHeader, " ")
|
||||
if len(splitted) != 2 {
|
||||
bcode.ReturnError(req, res, bcode.ErrNotAuthorized)
|
||||
return
|
||||
}
|
||||
tokenValue = splitted[1]
|
||||
}
|
||||
splitted := strings.Split(tokenHeader, " ")
|
||||
if len(splitted) != 2 {
|
||||
bcode.ReturnError(req, res, bcode.ErrNotAuthorized)
|
||||
return
|
||||
if tokenValue == "" {
|
||||
if strings.HasPrefix(req.Request.URL.Path, "/view") {
|
||||
tokenValue = req.QueryParameter("token")
|
||||
}
|
||||
if tokenValue == "" {
|
||||
bcode.ReturnError(req, res, bcode.ErrNotAuthorized)
|
||||
}
|
||||
}
|
||||
|
||||
token, err := service.ParseToken(splitted[1])
|
||||
token, err := service.ParseToken(tokenValue)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
|
|
@ -108,6 +117,7 @@ func authCheckFilter(req *restful.Request, res *restful.Response, chain *restful
|
|||
return
|
||||
}
|
||||
req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyUser, token.Username))
|
||||
req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyToken, tokenValue))
|
||||
|
||||
chain.ProcessFilter(req, res)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
Copyright 2022 The KubeVela 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 api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/koding/websocketproxy"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/service"
|
||||
apis "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
)
|
||||
|
||||
// CloudShellAPIInterface provide the API for preparing the cloud shell environment
|
||||
type CloudShellAPIInterface struct {
|
||||
RbacService service.RBACService `inject:""`
|
||||
CloudShellService service.CloudShellService `inject:""`
|
||||
}
|
||||
|
||||
// NewCloudShellAPIInterface create the cloudshell api instance
|
||||
func NewCloudShellAPIInterface() *CloudShellAPIInterface {
|
||||
return &CloudShellAPIInterface{}
|
||||
}
|
||||
|
||||
// GetWebServiceRoute -
|
||||
func (c *CloudShellAPIInterface) GetWebServiceRoute() *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.Path(versionPrefix+"/cloudshell").
|
||||
Consumes(restful.MIME_XML, restful.MIME_JSON).
|
||||
Produces(restful.MIME_JSON, restful.MIME_XML).
|
||||
Doc("api for cloudshell manage")
|
||||
|
||||
tags := []string{"cloudshell"}
|
||||
|
||||
ws.Route(ws.POST("/").To(c.prepareCloudShell).
|
||||
Doc("prepare the user's cloud shell environment").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(c.RbacService.CheckPerm("cloudshell", "create")).
|
||||
Returns(200, "OK", apis.CloudShellPrepareResponse{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes(apis.CloudShellPrepareResponse{}).Do(returns200, returns500))
|
||||
|
||||
ws.Filter(authCheckFilter)
|
||||
return ws
|
||||
}
|
||||
|
||||
func (c *CloudShellAPIInterface) prepareCloudShell(req *restful.Request, res *restful.Response) {
|
||||
prepare, err := c.CloudShellService.Prepare(req.Request.Context())
|
||||
// Write back response data
|
||||
if err != nil {
|
||||
if prepare == nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
prepare.Status = service.StatusFailed
|
||||
prepare.Message = err.Error()
|
||||
}
|
||||
if err := res.WriteEntity(prepare); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// CloudShellView provide the view handler
|
||||
type CloudShellView struct {
|
||||
RbacService service.RBACService `inject:""`
|
||||
CloudShellService service.CloudShellService `inject:""`
|
||||
}
|
||||
|
||||
// NewCloudShellView new cloud share
|
||||
func NewCloudShellView() *CloudShellView {
|
||||
return &CloudShellView{}
|
||||
}
|
||||
|
||||
// GetWebServiceRoute -
|
||||
func (c *CloudShellView) GetWebServiceRoute() *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.Path(viewPrefix+"/cloudshell").
|
||||
Consumes(restful.MIME_XML, restful.MIME_JSON).
|
||||
Produces(restful.MIME_JSON, restful.MIME_XML).
|
||||
Doc("api for cluster manage")
|
||||
|
||||
tags := []string{"cloudshell"}
|
||||
|
||||
ws.Route(ws.GET("/").To(c.proxy).
|
||||
Doc("prepare the user's cloud shell environment").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(c.RbacService.CheckPerm("cloudshell", "create")).
|
||||
Returns(200, "OK", apis.EmptyResponse{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes(apis.EmptyResponse{}).Do(returns200, returns500))
|
||||
|
||||
ws.Route(ws.GET("/{subpath:*}").To(c.proxy).
|
||||
Doc("prepare the user's cloud shell environment").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(c.RbacService.CheckPerm("cloudshell", "create")).
|
||||
Returns(200, "OK", apis.EmptyResponse{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes(apis.EmptyResponse{}).Do(returns200, returns500))
|
||||
|
||||
ws.Filter(authCheckFilter)
|
||||
return ws
|
||||
}
|
||||
|
||||
func (c *CloudShellView) proxy(req *restful.Request, res *restful.Response) {
|
||||
endpoint, err := c.CloudShellService.GetCloudShellEndpoint(req.Request.Context())
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if req.HeaderParameter("Upgrade") == "websocket" && req.HeaderParameter("Connection") == "Upgrade" {
|
||||
u, err := url.Parse("ws://" + endpoint)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
req.Request.URL.Path = strings.Replace(req.Request.URL.Path, "/view/cloudshell", "", 1)
|
||||
// proxy the websocket request
|
||||
proxy := websocketproxy.NewProxy(u)
|
||||
proxy.Upgrader = &websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(req *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
proxy.ServeHTTP(res.ResponseWriter, req.Request)
|
||||
return
|
||||
}
|
||||
u, err := url.Parse("http://" + endpoint)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
NewReverseProxy(u).ServeHTTP(res.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
// NewReverseProxy proxy for requests of the cloud shell
|
||||
func NewReverseProxy(target *url.URL) *httputil.ReverseProxy {
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = strings.Replace(req.URL.Path, "/view/cloudshell", "", 1)
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
|
|
@ -46,6 +46,8 @@ var (
|
|||
CtxKeyApplicationComponent = "component"
|
||||
// CtxKeyUser request context key of user
|
||||
CtxKeyUser = "user"
|
||||
// CtxKeyToken request context key of request token
|
||||
CtxKeyToken = "token"
|
||||
)
|
||||
|
||||
// AddonPhase defines the phase of an addon
|
||||
|
|
@ -1406,3 +1408,9 @@ type ImageRegistry struct {
|
|||
type ListImageRegistryResponse struct {
|
||||
Registries []ImageRegistry `json:"registries"`
|
||||
}
|
||||
|
||||
// CloudShellPrepareResponse the response for the cloud shell environment creation
|
||||
type CloudShellPrepareResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ import (
|
|||
// versionPrefix API version prefix.
|
||||
var versionPrefix = "/api/v1"
|
||||
|
||||
// viewPrefix the path prefix for view page
|
||||
var viewPrefix = "/view"
|
||||
|
||||
// Interface the API should define the http route
|
||||
type Interface interface {
|
||||
GetWebServiceRoute() *restful.WebService
|
||||
|
|
@ -77,11 +80,13 @@ func InitAPIBean() []interface{} {
|
|||
RegisterAPIInterface(NewVelaQLAPIInterface())
|
||||
RegisterAPIInterface(NewWebhookAPIInterface())
|
||||
RegisterAPIInterface(NewRepositoryAPIInterface())
|
||||
RegisterAPIInterface(NewCloudShellAPIInterface())
|
||||
|
||||
// Authentication
|
||||
RegisterAPIInterface(NewAuthenticationAPIInterface())
|
||||
RegisterAPIInterface(NewUserAPIInterface())
|
||||
RegisterAPIInterface(NewSystemInfoAPIInterface())
|
||||
RegisterAPIInterface(NewCloudShellView())
|
||||
|
||||
// RBAC
|
||||
RegisterAPIInterface(NewRBACAPIInterface())
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ import (
|
|||
)
|
||||
|
||||
func TestInitAPIBean(t *testing.T) {
|
||||
assert.Equal(t, len(InitAPIBean()), 20)
|
||||
assert.Equal(t, len(InitAPIBean()), 22)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ func (s *restServer) RegisterAPIRoute() restfulSpec.Config {
|
|||
|
||||
// Add container filter to respond to OPTIONS
|
||||
s.webContainer.Filter(s.webContainer.OPTIONSFilter)
|
||||
s.webContainer.Filter(s.OPTIONSFilter)
|
||||
|
||||
// Add request log
|
||||
s.webContainer.Filter(s.requestLog)
|
||||
|
|
@ -240,6 +241,10 @@ func (s *restServer) RegisterAPIRoute() restfulSpec.Config {
|
|||
}
|
||||
|
||||
func (s *restServer) requestLog(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
if req.HeaderParameter("Upgrade") == "websocket" && req.HeaderParameter("Connection") == "Upgrade" {
|
||||
chain.ProcessFilter(req, resp)
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
c := utils.NewResponseCapture(resp.ResponseWriter)
|
||||
resp.ResponseWriter = c
|
||||
|
|
@ -255,6 +260,14 @@ func (s *restServer) requestLog(req *restful.Request, resp *restful.Response, ch
|
|||
).Infof("request log")
|
||||
}
|
||||
|
||||
func (s *restServer) OPTIONSFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
if req.Request.Method != "OPTIONS" {
|
||||
chain.ProcessFilter(req, resp)
|
||||
return
|
||||
}
|
||||
resp.AddHeader(restful.HEADER_AccessControlAllowCredentials, "true")
|
||||
}
|
||||
|
||||
func enrichSwaggerObject(swo *spec.Swagger) {
|
||||
swo.Info = &spec.Info{
|
||||
InfoProps: spec.InfoProps{
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ import (
|
|||
// KubeVelaProjectGroupPrefix the prefix kubevela project
|
||||
const KubeVelaProjectGroupPrefix = "kubevela:project:"
|
||||
|
||||
// KubeVelaAdminGroupPrefix the prefix kubevela admin
|
||||
const KubeVelaAdminGroupPrefix = "kubevela:admin:"
|
||||
|
||||
// ContextWithUserInfo extract user from context (parse username and project) for impersonation
|
||||
func ContextWithUserInfo(ctx context.Context) context.Context {
|
||||
if !features.APIServerFeatureGate.Enabled(features.APIServerEnableImpersonation) {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,12 @@ var (
|
|||
|
||||
// ErrAddonInvalidVersion means add version is invalid
|
||||
ErrAddonInvalidVersion = NewBcode(400, 50019, "")
|
||||
|
||||
// ErrCloudShellAddonNotEnabled means the cloudshell CRD is not installed
|
||||
ErrCloudShellAddonNotEnabled = NewBcode(200, 50020, "Please enable the CloudShell addon first")
|
||||
|
||||
// ErrCloudShellNotInit means the cloudshell CR not created
|
||||
ErrCloudShellNotInit = NewBcode(400, 50021, "Closing the console window and retry")
|
||||
)
|
||||
|
||||
// isGithubRateLimit check if error is github rate limit
|
||||
|
|
|
|||
|
|
@ -30,9 +30,11 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
certificatesv1 "k8s.io/api/certificates/v1"
|
||||
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
|
|
@ -140,6 +142,8 @@ func newKubeConfigGenerateOptions(options ...KubeConfigGenerateOption) *KubeConf
|
|||
const (
|
||||
// KubeVelaClientGroup the default group to be added to the generated X509 KubeConfig
|
||||
KubeVelaClientGroup = "kubevela:client"
|
||||
// CSRNamePrefix the prefix of the CSR name
|
||||
CSRNamePrefix = "kubevela-csr"
|
||||
)
|
||||
|
||||
// GenerateKubeConfig generate KubeConfig for users with given options.
|
||||
|
|
@ -153,24 +157,40 @@ func GenerateKubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clie
|
|||
return nil, errors.New("either x509 or serviceaccount must be set for creating KubeConfig")
|
||||
}
|
||||
|
||||
func genKubeConfig(cfg *clientcmdapi.Config, authInfo *clientcmdapi.AuthInfo, caData []byte) *clientcmdapi.Config {
|
||||
func genKubeConfig(cfg *clientcmdapi.Config, authInfo *clientcmdapi.AuthInfo, caData []byte) (*clientcmdapi.Config, error) {
|
||||
if len(cfg.Clusters) == 0 {
|
||||
return nil, fmt.Errorf("there is no clusters in the cluster config")
|
||||
}
|
||||
exportCfg := cfg.DeepCopy()
|
||||
exportContext := cfg.Contexts[cfg.CurrentContext].DeepCopy()
|
||||
exportCfg.Contexts = map[string]*clientcmdapi.Context{cfg.CurrentContext: exportContext}
|
||||
var exportContext *clientcmdapi.Context
|
||||
if len(cfg.Contexts) > 0 {
|
||||
exportContext = cfg.Contexts[cfg.CurrentContext].DeepCopy()
|
||||
exportCfg.Contexts = map[string]*clientcmdapi.Context{cfg.CurrentContext: exportContext}
|
||||
} else {
|
||||
exportCfg.Contexts = map[string]*clientcmdapi.Context{}
|
||||
for name := range cfg.Clusters {
|
||||
exportContext = &clientcmdapi.Context{
|
||||
Cluster: name,
|
||||
AuthInfo: authInfo.Username,
|
||||
}
|
||||
exportCfg.Contexts["local"] = exportContext
|
||||
}
|
||||
exportCfg.CurrentContext = "local"
|
||||
}
|
||||
exportCluster := cfg.Clusters[exportContext.Cluster].DeepCopy()
|
||||
if caData != nil {
|
||||
exportCluster.CertificateAuthorityData = caData
|
||||
}
|
||||
exportCfg.Clusters = map[string]*clientcmdapi.Cluster{exportContext.Cluster: exportCluster}
|
||||
exportCfg.AuthInfos = map[string]*clientcmdapi.AuthInfo{exportContext.AuthInfo: authInfo}
|
||||
return exportCfg
|
||||
return exportCfg, nil
|
||||
}
|
||||
|
||||
func generateX509KubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
|
||||
func makeCertAndKey(writer io.Writer, opts *KubeConfigGenerateX509Options) ([]byte, []byte, error) {
|
||||
// generate private key
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, opts.PrivateKeyBits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
keyBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
|
||||
_, _ = fmt.Fprintf(writer, "Private key generated.\n")
|
||||
|
|
@ -185,21 +205,41 @@ func generateX509KubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *
|
|||
|
||||
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
csrPemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
|
||||
_, _ = fmt.Fprintf(writer, "Certificate request generated.\n")
|
||||
return csrPemBytes, keyBytes, nil
|
||||
}
|
||||
|
||||
func makeCSRName(user string) string {
|
||||
return fmt.Sprintf("%s-%s", CSRNamePrefix, user)
|
||||
}
|
||||
|
||||
func generateX509KubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, options *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
|
||||
info, _ := cli.Discovery().ServerVersion()
|
||||
if info == nil || version.MustParseGeneric(info.String()).AtLeast(version.MustParseSemantic("v1.19.0")) {
|
||||
|
||||
return generateX509KubeConfigV1(ctx, cli, cfg, writer, options)
|
||||
}
|
||||
return generateX509KubeConfigV1Beta(ctx, cli, cfg, writer, options)
|
||||
}
|
||||
|
||||
func generateX509KubeConfigV1(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
|
||||
csrPemBytes, keyBytes, err := makeCertAndKey(writer, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csr := &certificatesv1.CertificateSigningRequest{}
|
||||
csr.Name = opts.User
|
||||
csr.Name = makeCSRName(opts.User)
|
||||
csr.Spec.SignerName = certificatesv1.KubeAPIServerClientSignerName
|
||||
csr.Spec.Usages = []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth}
|
||||
csr.Spec.Request = csrPemBytes
|
||||
csr.Spec.ExpirationSeconds = pointer.Int32(int32(opts.ExpireTime.Seconds()))
|
||||
if csr, err = cli.CertificatesV1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}); err != nil {
|
||||
if _, err := cli.CertificatesV1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = fmt.Fprintf(writer, "Certificate signing request %s generated.\n", opts.User)
|
||||
_, _ = fmt.Fprintf(writer, "Certificate signing request %s generated.\n", csr.Name)
|
||||
defer func() {
|
||||
_ = cli.CertificatesV1().CertificateSigningRequests().Delete(ctx, csr.Name, metav1.DeleteOptions{})
|
||||
}()
|
||||
|
|
@ -213,9 +253,10 @@ func generateX509KubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *
|
|||
if csr, err = cli.CertificatesV1().CertificateSigningRequests().UpdateApproval(ctx, csr.Name, csr, metav1.UpdateOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = fmt.Fprintf(writer, "Certificate signing request %s approved.\n", opts.User)
|
||||
if err = wait.Poll(time.Second, time.Minute, func() (done bool, err error) {
|
||||
if csr, err = cli.CertificatesV1().CertificateSigningRequests().Get(ctx, opts.User, metav1.GetOptions{}); err != nil {
|
||||
_, _ = fmt.Fprintf(writer, "Certificate signing request %s approved.\n", csr.Name)
|
||||
|
||||
if err := wait.Poll(time.Second, time.Minute, func() (done bool, err error) {
|
||||
if csr, err = cli.CertificatesV1().CertificateSigningRequests().Get(ctx, csr.Name, metav1.GetOptions{}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if csr.Status.Certificate == nil {
|
||||
|
|
@ -230,7 +271,61 @@ func generateX509KubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *
|
|||
return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
|
||||
ClientKeyData: keyBytes,
|
||||
ClientCertificateData: csr.Status.Certificate,
|
||||
}, nil), nil
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func generateX509KubeConfigV1Beta(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
|
||||
csrPemBytes, keyBytes, err := makeCertAndKey(writer, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csr := &certificatesv1beta1.CertificateSigningRequest{}
|
||||
csr.Name = makeCSRName(opts.User)
|
||||
var name = certificatesv1beta1.KubeAPIServerClientSignerName
|
||||
csr.Spec.SignerName = &name
|
||||
csr.Spec.Usages = []certificatesv1beta1.KeyUsage{certificatesv1beta1.UsageClientAuth}
|
||||
csr.Spec.Request = csrPemBytes
|
||||
csr.Spec.ExpirationSeconds = pointer.Int32(int32(opts.ExpireTime.Seconds()))
|
||||
// create
|
||||
if _, err = cli.CertificatesV1beta1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = fmt.Fprintf(writer, "Certificate signing request %s generated.\n", csr.Name)
|
||||
defer func() {
|
||||
_ = cli.CertificatesV1beta1().CertificateSigningRequests().Delete(ctx, csr.Name, metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
// approval
|
||||
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{
|
||||
Type: certificatesv1beta1.CertificateApproved,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "Self-generated and auto-approved by KubeVela",
|
||||
Message: "This CSR was approved by KubeVela",
|
||||
LastUpdateTime: metav1.Now(),
|
||||
})
|
||||
if csr, err = cli.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(ctx, csr, metav1.UpdateOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = fmt.Fprintf(writer, "Certificate signing request %s approved.\n", csr.Name)
|
||||
|
||||
// waiting and get the status
|
||||
if err = wait.Poll(time.Second, time.Minute, func() (done bool, err error) {
|
||||
if csr, err = cli.CertificatesV1beta1().CertificateSigningRequests().Get(ctx, csr.Name, metav1.GetOptions{}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if csr.Status.Certificate == nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = fmt.Fprintf(writer, "Signed certificate retrieved.\n")
|
||||
|
||||
return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
|
||||
ClientKeyData: keyBytes,
|
||||
ClientCertificateData: csr.Status.Certificate,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func generateServiceAccountKubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateServiceAccountOptions) (*clientcmdapi.Config, error) {
|
||||
|
|
@ -257,7 +352,7 @@ func generateServiceAccountKubeConfig(ctx context.Context, cli kubernetes.Interf
|
|||
_, _ = fmt.Fprintf(writer, "ServiceAccount token found.\n")
|
||||
return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
|
||||
Token: string(secret.Data["token"]),
|
||||
}, secret.Data["ca.crt"]), nil
|
||||
}, secret.Data["ca.crt"])
|
||||
}
|
||||
|
||||
// ReadIdentityFromKubeConfig extract identity from kubeconfig
|
||||
|
|
|
|||
|
|
@ -249,6 +249,10 @@ const (
|
|||
KubeVelaReaderRoleName = "kubevela:reader"
|
||||
// KubeVelaWriterRoleName a role that can read/write any resources
|
||||
KubeVelaWriterRoleName = "kubevela:writer"
|
||||
// KubeVelaWriterAppRoleName a role that can read/write any application
|
||||
KubeVelaWriterAppRoleName = "kubevela:writer:application"
|
||||
// KubeVelaReaderAppRoleName a role that can read any application
|
||||
KubeVelaReaderAppRoleName = "kubevela:reader:application"
|
||||
)
|
||||
|
||||
// ScopedPrivilege includes all resource privileges in the destination
|
||||
|
|
@ -287,11 +291,9 @@ func (p *ScopedPrivilege) GetRoles() []client.Object {
|
|||
// GetRoleBinding the underlying RoleBinding/ClusterRoleBinding for the privilege
|
||||
func (p *ScopedPrivilege) GetRoleBinding(subs []rbacv1.Subject) client.Object {
|
||||
var binding client.Object
|
||||
var roleName string
|
||||
var roleName = KubeVelaWriterRoleName
|
||||
if p.ReadOnly {
|
||||
roleName = KubeVelaReaderRoleName
|
||||
} else {
|
||||
roleName = KubeVelaWriterRoleName
|
||||
}
|
||||
if p.Namespace == "" {
|
||||
binding = &rbacv1.ClusterRoleBinding{
|
||||
|
|
@ -309,6 +311,64 @@ func (p *ScopedPrivilege) GetRoleBinding(subs []rbacv1.Subject) client.Object {
|
|||
return binding
|
||||
}
|
||||
|
||||
// ApplicationPrivilege includes the application privileges in the destination
|
||||
type ApplicationPrivilege struct {
|
||||
Prefix string
|
||||
Cluster string
|
||||
Namespace string
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
// GetCluster the cluster of the privilege
|
||||
func (a *ApplicationPrivilege) GetCluster() string {
|
||||
return a.Cluster
|
||||
}
|
||||
|
||||
// GetRoles the underlying Roles/ClusterRoles for the privilege
|
||||
func (a *ApplicationPrivilege) GetRoles() []client.Object {
|
||||
verbs := []string{"get", "list", "watch", "create", "update", "patch", "delete"}
|
||||
name := a.Prefix + KubeVelaWriterAppRoleName
|
||||
if a.ReadOnly {
|
||||
verbs = []string{"get", "list", "watch"}
|
||||
name = a.Prefix + KubeVelaReaderAppRoleName
|
||||
}
|
||||
return []client.Object{
|
||||
&rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"core.oam.dev"},
|
||||
Resources: []string{"applications", "policies", "workflows"},
|
||||
Verbs: verbs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetRoleBinding the underlying RoleBinding/ClusterRoleBinding for the privilege
|
||||
func (a *ApplicationPrivilege) GetRoleBinding(subs []rbacv1.Subject) client.Object {
|
||||
var binding client.Object
|
||||
var roleName = KubeVelaWriterAppRoleName
|
||||
if a.ReadOnly {
|
||||
roleName = KubeVelaReaderAppRoleName
|
||||
}
|
||||
if a.Namespace == "" {
|
||||
binding = &rbacv1.ClusterRoleBinding{
|
||||
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
|
||||
Subjects: subs,
|
||||
}
|
||||
} else {
|
||||
binding = &rbacv1.RoleBinding{
|
||||
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: roleName},
|
||||
Subjects: subs,
|
||||
}
|
||||
binding.SetNamespace(a.Namespace)
|
||||
}
|
||||
binding.SetName(a.Prefix + roleName + ":binding")
|
||||
return binding
|
||||
}
|
||||
|
||||
func mergeSubjects(src []rbacv1.Subject, merge []rbacv1.Subject) []rbacv1.Subject {
|
||||
subs := append([]rbacv1.Subject{}, src...)
|
||||
for _, sub := range merge {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import (
|
|||
"cuelang.org/go/cue/format"
|
||||
"cuelang.org/go/encoding/openapi"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
cloudshellv1alpha1 "github.com/cloudtty/cloudtty/pkg/apis/cloudshell/v1alpha1"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/oam-dev/terraform-config-inspect/tfconfig"
|
||||
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
|
|
@ -106,6 +107,7 @@ func init() {
|
|||
_ = metricsV1beta1api.AddToScheme(Scheme)
|
||||
_ = kruisev1alpha1.AddToScheme(Scheme)
|
||||
_ = prismclusterv1alpha1.AddToScheme(Scheme)
|
||||
_ = cloudshellv1alpha1.AddToScheme(Scheme)
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ var _ = Describe("Test multicluster Auth commands", func() {
|
|||
It("Test vela create kubeconfig for given user", func() {
|
||||
outputs, err := execCommand("auth", "gen-kubeconfig", "--user", "kubevela", "--group", "kubevela:dev", "--group", "kubevela:test")
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(outputs).Should(ContainSubstring("Certificate signing request kubevela approved"))
|
||||
Expect(outputs).Should(ContainSubstring("Certificate signing request kubevela-csr-kubevela approved"))
|
||||
})
|
||||
|
||||
It("Test vela create kubeconfig for serviceaccount", func() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue