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:
Jianbo Sun 2022-07-01 23:31:15 +08:00
parent 2af8ab13d6
commit eea8570e10
38 changed files with 1538 additions and 119 deletions

View File

@ -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:

3
.gitignore vendored
View File

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

31
Dockerfile.cloudshell Normal file
View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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"))
})
})

View File

@ -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)}
}

View File

@ -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"},
}

View File

@ -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)
}
}
}
}

View File

@ -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
}

View File

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

View File

@ -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
}

View File

@ -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())

View File

@ -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(),
}
}

View File

@ -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}
}

View File

@ -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: []

View File

@ -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},
}
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}
}

View File

@ -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)

View File

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

View File

@ -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))

View File

@ -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{}).

View File

@ -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)
}

View File

@ -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}
}

View File

@ -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"`
}

View File

@ -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())

View File

@ -23,5 +23,5 @@ import (
)
func TestInitAPIBean(t *testing.T) {
assert.Equal(t, len(InitAPIBean()), 20)
assert.Equal(t, len(InitAPIBean()), 22)
}

View File

@ -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{

View File

@ -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) {

View File

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

View File

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

View File

@ -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 {

View File

@ -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
}

View File

@ -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() {