Compare commits

...

47 Commits

Author SHA1 Message Date
Somefive eee4e07950
fix: cmd/apiserver/Dockerfile to reduce vulnerabilities (#56)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386
- https://snyk.io/vuln/SNYK-ALPINE318-BUSYBOX-7249265
- https://snyk.io/vuln/SNYK-ALPINE318-BUSYBOX-7249419
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6055795

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-12-13 16:53:54 +08:00
Jianbo Sun 1ecdbb4aca
Merge pull request #42 from kubevela/snyk-fix-3ea40a1c7421c1fa7752257e129041ca
[Snyk] Security upgrade alpine from 3.17 to 3.18.3
2023-11-02 17:32:32 +08:00
snyk-bot 01dd83251a
fix: cmd/apiserver/Dockerfile to reduce vulnerabilities 2023-10-26 02:32:29 +00:00
Yin Da 88d23063b9 Refactor: refactor vela-prism dockerfile for arm arch
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2023-06-06 19:11:47 +08:00
Somefive c12eae92d0
Merge pull request #38 from Somefive/feat/update-ci
Feat: update ci for build-image and helm-chart
2023-06-05 17:22:57 +08:00
Yin Da dc809e6c77 Feat: update ci for build-image and helm-chart
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2023-06-05 17:22:03 +08:00
Somefive b7e45f7f84
Merge pull request #37 from Somefive/feat/upgrade-go-version
Feat: upgrade k8s.io to 0.26
2023-04-03 11:05:19 +08:00
Yin Da cad8574718 Feat: upgrade go version
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2023-04-03 10:54:31 +08:00
Somefive 946eb7a2ce
Merge pull request #34 from Somefive/refactor/common-pkg
Refactor: move common pkg to kubevela/pkg
2023-03-28 14:11:34 +08:00
Yin Da eee3f1c2cc Refactor: move common pkg to kubevela/pkg
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2023-02-22 11:58:28 +08:00
Somefive d67ab30eb9
Merge pull request #31 from Somefive/fix/add-ignore-namespace-for-list-managed-cluster
Fix: add ignore namespace for list managed cluster
2022-11-18 16:10:54 +08:00
Somefive 8026c15cec
Merge pull request #30 from Somefive/feat/dynamic-api
Feat: add dynamic api
2022-11-18 14:52:22 +08:00
Yin Da 51eb282528 Feat: upgrade cluster-gateway dependency
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-11-18 14:28:13 +08:00
Yin Da fd6779756c Test: add test for singleton & dynamic api
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-11-17 15:42:26 +08:00
Yin Da 2aca265923 Fix: add ignore namespace for list managed cluster
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-11-17 12:01:01 +08:00
Yin Da 6e77b01d61 Feat: add dynamic api
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-11-16 12:32:55 +08:00
Somefive c431248cb0
Merge pull request #29 from Somefive/feat/add-min-request-timeout-arg
Feat: add min-request-timeout args
2022-10-24 16:58:28 +08:00
Yin Da bb878067ac Feat: add min-request-timeout args
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-10-24 16:57:57 +08:00
Somefive 42496db81b
Merge pull request #28 from Somefive/feat/upgrade-go
Feat: upgrade go version
2022-10-24 16:46:01 +08:00
Yin Da bff60a0474 Feat: upgrade go version
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-10-24 16:30:53 +08:00
Somefive 6a25625e53
Merge pull request #27 from Somefive/feat/add-request-timeout-options
Feat: add request timeout options
2022-10-21 16:38:05 +08:00
Yin Da faad0deebb Feat: add request timeout options
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-10-21 15:58:03 +08:00
Somefive 5dd2da483c
Merge pull request #26 from Somefive/feat/remove-owner-reference
Feat: remove owner reference for grafana secret
2022-10-18 16:45:11 +08:00
Yin Da 9463fc48de Feat: remove owner reference for grafana secret
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-10-18 16:29:10 +08:00
Somefive 6bf3ad33f8
Merge pull request #25 from Somefive/feat/support-cluster-control-plane-label
Fix: multicluster client panic bug
2022-09-15 15:19:49 +08:00
Yin Da c3fb485060 Fix: panic bug
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-09-15 14:36:30 +08:00
Somefive c752b9d821
Merge pull request #24 from Somefive/feat/support-cluster-control-plane-label
Feat: support cluster control plane label
2022-09-15 11:43:47 +08:00
Yin Da 0b780bccf0 Feat: support cluster control plane label
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-09-15 11:33:34 +08:00
Somefive b343320c2e
Merge pull request #22 from Somefive/fix/update-readme
Chore: update readme for grafana
2022-07-28 15:08:18 +08:00
Yin Da b2d7fec4ef Chore: update readme for grafana
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-28 15:05:48 +08:00
Somefive c3c6d996c2
Merge pull request #21 from Somefive/fix/create-dashboard-wo-id
Fix: delete id for dashboard request
2022-07-26 20:49:45 +08:00
Yin Da 245a784aeb Feat: delete id for dashboard request
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-26 20:37:45 +08:00
Somefive 6268967fe5
Merge pull request #20 from Somefive/feat/remove_acr_gh_action
Feat: remove acr image build
2022-07-26 11:30:59 +08:00
Yin Da dfe1d1910f Feat: remove acr image build
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-26 11:24:38 +08:00
Somefive 3cbc86010c
Merge pull request #19 from Somefive/feat/remove_acr_gh_action
Fix: update acr to kubevela.net in github action for build image
2022-07-26 11:01:42 +08:00
Yin Da bdcaaa8f3c Feat: update helm chart CI
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-26 10:59:16 +08:00
Somefive 5f395adaeb
Merge pull request #18 from Somefive/feat/grafana-config
Feat: add support for Grafana & GrafanaDashboard & GrafanaDatasource
2022-07-22 21:01:31 +08:00
Yin Da 21f02c3a7a Chore: minor fixes
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-22 20:39:12 +08:00
Yin Da dba38778cd Chore: add mock server test
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-22 20:09:32 +08:00
Yin Da 47de5e92c5 Chore: add conversion test
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-22 17:37:50 +08:00
Yin Da e0e88c8b42 Chore: bootstrap test
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-22 16:01:22 +08:00
Yin Da 03f16c1944 Feat: update charts
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-22 15:18:26 +08:00
Yin Da b22b06bdcd Feat: complete grafana/grafanadashboard/grafanadatasource
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-22 15:16:52 +08:00
Yin Da c803cd0951 Feat: bootstrap grafana resources
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-07-21 19:34:11 +08:00
Somefive 94f1190f87
Merge pull request #17 from Somefive/fix/support-ocm-cluster-secret
Feat: filter out OCM ManagedCluster underlying secret
2022-06-13 20:34:57 +08:00
Yin Da 15fe69a2ad Feat: filter out OCM ManagedCluster underlying secret
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-06-13 20:25:40 +08:00
Yin Da 27c9883d98 Fix: add health probe for deployment
Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
2022-05-27 11:44:02 +08:00
75 changed files with 5269 additions and 1283 deletions

57
.github/workflows/build-image.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: BuildImage
on:
push:
branches:
- master
- release-*
tags:
- 'v*'
workflow_dispatch: { }
jobs:
build-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
oamdev/vela-prism
ghcr.io/kubevela/oamdev/vela-prism
tags: |
type=ref,event=tag
type=raw,value=latest,enable={{is_default_branch}}
- name: Login docker.io
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
file: ./cmd/apiserver/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

67
.github/workflows/chart.yml vendored Normal file
View File

@ -0,0 +1,67 @@
name: HelmChart
on:
push:
tags:
- "v*"
workflow_dispatch: {}
jobs:
publish-charts:
env:
HELM_CHART: charts/
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Get the vars
id: vars
run: |
echo ::set-output name=TAG::${GITHUB_REF#refs/tags/}
- name: Install Helm
uses: azure/setup-helm@v1
with:
version: v3.4.0
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '14'
- uses: oprypin/find-latest-tag@v1
with:
repository: kubevela/workflow
releases-only: true
id: latest_tag
- name: Tag helm chart image
run: |
latest_repo_tag=${{ steps.latest_tag.outputs.tag }}
sub="."
major="$(cut -d"$sub" -f1 <<<"$latest_repo_tag")"
minor="$(cut -d"$sub" -f2 <<<"$latest_repo_tag")"
patch="0"
current_repo_tag="$major.$minor.$patch"
image_tag=${GITHUB_REF#refs/tags/}
chart_version=$latest_repo_tag
if [[ ${GITHUB_REF} == "refs/heads/main" ]]; then
image_tag=latest
chart_version=${current_repo_tag}-nightly-build
fi
sed -i "s/latest/${image_tag}/g" $HELM_CHART/values.yaml
chart_smever=${chart_version#"v"}
sed -i "s/0.1.0/$chart_smever/g" $HELM_CHART/Chart.yaml
- uses: jnwng/github-app-installation-token-action@v2
id: get_app_token
with:
appId: 340472
installationId: 38064967
privateKey: ${{ secrets.GH_KUBEVELA_APP_PRIVATE_KEY }}
- name: Sync Chart Repo
run: |
git config --global user.email "135009839+kubevela[bot]@users.noreply.github.com"
git config --global user.name "kubevela[bot]"
git clone https://x-access-token:${{ steps.get_app_token.outputs.token }}@github.com/kubevela/charts.git kubevela-charts
helm package $HELM_CHART --destination ./kubevela-charts/docs/
helm repo index --url https://kubevela.github.io/charts ./kubevela-charts/docs/
cd kubevela-charts/
git add docs/
chart_version=${GITHUB_REF#refs/tags/}
git commit -m "update vela-prism chart ${chart_version}"
git push https://x-access-token:${{ steps.get_app_token.outputs.token }}@github.com/kubevela/charts.git

View File

@ -1,117 +0,0 @@
name: HelmChart
on:
push:
tags:
- "v*"
workflow_dispatch: {}
env:
BUCKET: ${{ secrets.OSS_BUCKET }}
ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
ACCESS_KEY: ${{ secrets.OSS_ACCESS_KEY }}
ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
jobs:
publish-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Get the vars
id: vars
run: |
echo ::set-output name=TAG::${GITHUB_REF#refs/tags/}
- name: Login ghcr.io
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login Alibaba Cloud ACR
uses: docker/login-action@v1
with:
registry: kubevela-registry.cn-hangzhou.cr.aliyuncs.com
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
with:
driver-opts: image=moby/buildkit:master
- name: Build & Pushing vela-prism for ACR
run: |
docker build --build-arg GOPROXY=https://proxy.golang.org -t kubevela-registry.cn-hangzhou.cr.aliyuncs.com/oamdev/vela-prism:${{ steps.vars.outputs.TAG }} -f ./cmd/apiserver/Dockerfile .
docker push kubevela-registry.cn-hangzhou.cr.aliyuncs.com/oamdev/vela-prism:${{ steps.vars.outputs.TAG }}
- uses: docker/build-push-action@v2
name: Build & Pushing vela-prism for Dockerhub and GHCR
with:
context: .
file: ./cmd/apiserver/Dockerfile
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: |
GOPROXY=https://proxy.golang.org
tags: |-
docker.io/oamdev/vela-prism:${{ steps.vars.outputs.TAG }}
ghcr.io/${{ github.repository }}/vela-prism:${{ steps.vars.outputs.TAG }}
publish-charts:
env:
HELM_CHART: charts/
LOCAL_OSS_DIRECTORY: .oss/
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@master
- name: Get the vars
id: vars
run: |
echo ::set-output name=TAG::${GITHUB_REF#refs/tags/}
- name: Install Helm
uses: azure/setup-helm@v1
with:
version: v3.4.0
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '14'
- uses: oprypin/find-latest-tag@v1
with:
repository: kubevela/prism
releases-only: true
id: latest_tag
- name: Tag helm chart image
run: |
latest_repo_tag=${{ steps.latest_tag.outputs.tag }}
sub="."
major="$(cut -d"$sub" -f1 <<<"$latest_repo_tag")"
minor="$(cut -d"$sub" -f2 <<<"$latest_repo_tag")"
patch="0"
current_repo_tag="$major.$minor.$patch"
image_tag=${GITHUB_REF#refs/tags/}
chart_version=$latest_repo_tag
if [[ ${GITHUB_REF} == "refs/heads/master" ]]; then
image_tag=latest
chart_version=${current_repo_tag}-nightly-build
fi
sed -i "s/latest/${image_tag}/g" $HELM_CHART/values.yaml
chart_smever=${chart_version#"v"}
sed -i "s/0.1.0/$chart_smever/g" $HELM_CHART/Chart.yaml
- name: Install ossutil
run: wget http://gosspublic.alicdn.com/ossutil/1.7.0/ossutil64 && chmod +x ossutil64 && mv ossutil64 ossutil
- name: Configure Alibaba Cloud OSSUTIL
run: ./ossutil --config-file .ossutilconfig config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
- name: sync cloud to local
run: ./ossutil --config-file .ossutilconfig sync oss://$BUCKET/prism $LOCAL_OSS_DIRECTORY
- name: Package helm charts
run: |
helm package $HELM_CHART --destination $LOCAL_OSS_DIRECTORY
helm repo index --url https://$BUCKET.$ENDPOINT/prism $LOCAL_OSS_DIRECTORY
- name: sync local to cloud
run: ./ossutil --config-file .ossutilconfig sync $LOCAL_OSS_DIRECTORY oss://$BUCKET/prism -f

View File

@ -1,86 +0,0 @@
name: PostSubmit
on:
push:
branches:
- master
workflow_dispatch: {}
env:
GO_VERSION: '1.18'
jobs:
detect-noop:
runs-on: ubuntu-20.04
outputs:
noop: ${{ steps.noop.outputs.should_skip }}
steps:
- name: Detect No-op Changes
id: noop
uses: fkirc/skip-duplicate-actions@v3.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
paths_ignore: '["**.md", "**.mdx", "**.png", "**.jpg"]'
do_not_skip: '["workflow_dispatch", "schedule", "push"]'
concurrent_skipping: false
image-multi-arch:
runs-on: ubuntu-20.04
needs: detect-noop
if: needs.detect-noop.outputs.noop != 'true'
strategy:
matrix:
arch: [ amd64, arm64 ]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- name: Build Image
run: |
IMG_TAG=latest-${{ matrix.arch }} \
OS=linux \
ARCH=${{ matrix.arch }} \
make image-apiserver
- name: Push Image
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login --username ${{ secrets.DOCKER_USER }} --password-stdin
docker push oamdev/vela-prism:latest-${{ matrix.arch }}
docker push oamdev/vela-prism:latest-${{ matrix.arch }}
image-manifest:
runs-on: ubuntu-latest
needs: [ image-multi-arch ]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Create Manifest
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login --username ${{ secrets.DOCKER_USER }} --password-stdin
docker manifest create oamdev/vela-prism:latest \
oamdev/vela-prism:latest-amd64 \
oamdev/vela-prism:latest-arm64
- name: Annotate Manifest
run: |
docker manifest annotate oamdev/vela-prism:latest \
oamdev/vela-prism:latest-amd64 --arch amd64
docker manifest annotate oamdev/vela-prism:latest \
oamdev/vela-prism:latest-arm64 --arch arm64
- name: Push Manifest
run: |
docker manifest push oamdev/vela-prism:latest

View File

@ -12,7 +12,7 @@ on:
- release-*
env:
GO_VERSION: '1.18'
GO_VERSION: '1.19'
jobs:

4
.gitignore vendored
View File

@ -29,4 +29,6 @@ default.etcd/**
*.tgz
index.yaml
*.test
*.test
hack/debug

View File

@ -56,3 +56,99 @@ Therefore, the credential information will be secured and the user can also use
After installing vela-prism in your cluster, you can run `kubectl get vela-clusters` to view all the installed clusters.
> Notice that the vela-prism bootstrap parameter contains `--storage-namespace`, which identifies the underlying namespace for storing cluster secrets and the OCM managed cluster.
### Grafana related APIs
![PrismGrafanaArch](https://github.com/kubevela/prism/blob/master/hack/prism-grafana-arch.jpg)
#### Grafana
In vela-prism, you can store grafana access into Grafana object. The Grafana object is projected into secrets in `o11y-system` (this can be configured through `--observability-namespace` parameter).
The secret embeds the access endpoint and credential (either username/password for BasicAuth or token for BearerToken) for grafana. These will be used for communicating with Grafana APIs. Example of Grafana object is shown below.
```yaml
apiVersion: o11y.prism.oam.dev/v1alpha1
kind: Grafana
metadata:
name: example
spec:
access:
username: admin
password: kubevela
endpoint: https://grafana.o11y-system:3000/
```
#### GrafanaDashboard & GrafanaDatasource
After creating the Grafana object into the control plane, you are now able to manipulate Grafana resources through Kubernetes APIs now.
Currently, vela-prism provides proxies for GrafanaDatasource and GrafanaDashboard.
Their names are constructed by two parts, its original name and the backend grafana name.
For example, you can create a new GrafanaDashboard by applying the following YAML file. This will use the Grafana object above as the access credential, and call the Grafana APIs to create a new dashboard.
You can also update or delete dashboards or datasources. The spec part of GrafanaDashboard and GrafanaDatasource are directly projected into API request body.
```yaml
apiVersion: o11y.prism.oam.dev/v1alpha1
kind: GrafanaDashboard
metadata:
name: dashboard-test@example
spec:
title: New dashboard
tags: []
style: dark
timezone: browser
editable: true
hideControls: false
graphTooltip: 1
panels: []
time:
from: now-6h
to: now
timepicker:
time_options: []
refresh_intervals: []
templating:
list: []
annotations:
list: []
refresh: 5s
schemaVersion: 17
version: 0
links: []
```
Another example for GrafanaDatasource.
```yaml
apiVersion: o11y.prism.oam.dev/v1alpha1
kind: GrafanaDatasource
metadata:
name: prom-test@example
spec:
access: proxy
basicAuth: false
isDefault: false
name: ExamplePrometheus
readOnly: true
type: prometheus
url: https://prometheus-server.o11y-system:9090
```
#### verse operator pattern
To operate Grafana instances in Kubernetes, there are also [Grafana operators](https://github.com/grafana-operator/grafana-operator) to help manage Grafana configurations.
Compared to operator pattern, the aggregator pattern has pros and cons.
##### Pros
- There is no data consistency problem between the CustomResource and the Grafana underlying storage.
- API requests are made in time. The response is also immediate. No need for checking CustomResource status repeatedly.
- No reconciles. No need to hold CPUs and memories as controller does.
- Easy to connect with third-party Grafana instance. For example, Grafana from cloud providers.
##### Cons
- Cannot persist data outside Grafana storage. Once grafana is broken, the CustomResource will be unavailable as well.
The main drawback for vela-prism compared to operator pattern is that it cannot persist configurations. However, this can be solved through using KubeVela application to manage those configurations.
For example, you can write KubeVela applications to hold the dashboard configurations, instead of create another separate CustomResource. With KubeVela application, leverage GitOps is also possible.

View File

@ -56,3 +56,99 @@ Therefore, the credential information will be secured and the user can also use
After installing vela-prism in your cluster, you can run `kubectl get vela-clusters` to view all the installed clusters.
> Notice that the vela-prism bootstrap parameter contains `--storage-namespace`, which identifies the underlying namespace for storing cluster secrets and the OCM managed cluster.
### Grafana related APIs
![PrismGrafanaArch](https://github.com/kubevela/prism/blob/master/hack/prism-grafana-arch.jpg)
#### Grafana
In vela-prism, you can store grafana access into Grafana object. The Grafana object is projected into secrets in `o11y-system` (this can be configured through `--observability-namespace` parameter).
The secret embeds the access endpoint and credential (either username/password for BasicAuth or token for BearerToken) for grafana. These will be used for communicating with Grafana APIs. Example of Grafana object is shown below.
```yaml
apiVersion: o11y.prism.oam.dev/v1alpha1
kind: Grafana
metadata:
name: example
spec:
access:
username: admin
password: kubevela
endpoint: https://grafana.o11y-system:3000/
```
#### GrafanaDashboard & GrafanaDatasource
After creating the Grafana object into the control plane, you are now able to manipulate Grafana resources through Kubernetes APIs now.
Currently, vela-prism provides proxies for GrafanaDatasource and GrafanaDashboard.
Their names are constructed by two parts, its original name and the backend grafana name.
For example, you can create a new GrafanaDashboard by applying the following YAML file. This will use the Grafana object above as the access credential, and call the Grafana APIs to create a new dashboard.
You can also update or delete dashboards or datasources. The spec part of GrafanaDashboard and GrafanaDatasource are directly projected into API request body.
```yaml
apiVersion: o11y.prism.oam.dev/v1alpha1
kind: GrafanaDashboard
metadata:
name: dashboard-test@example
spec:
title: New dashboard
tags: []
style: dark
timezone: browser
editable: true
hideControls: false
graphTooltip: 1
panels: []
time:
from: now-6h
to: now
timepicker:
time_options: []
refresh_intervals: []
templating:
list: []
annotations:
list: []
refresh: 5s
schemaVersion: 17
version: 0
links: []
```
Another example for GrafanaDatasource.
```yaml
apiVersion: o11y.prism.oam.dev/v1alpha1
kind: GrafanaDatasource
metadata:
name: prom-test@example
spec:
access: proxy
basicAuth: false
isDefault: false
name: ExamplePrometheus
readOnly: true
type: prometheus
url: https://prometheus-server.o11y-system:9090
```
#### verse operator pattern
To operate Grafana instances in Kubernetes, there are also [Grafana operators](https://github.com/grafana-operator/grafana-operator) to help manage Grafana configurations.
Compared to operator pattern, the aggregator pattern has pros and cons.
##### Pros
- There is no data consistency problem between the CustomResource and the Grafana underlying storage.
- API requests are made in time. The response is also immediate. No need for checking CustomResource status repeatedly.
- No reconciles. No need to hold CPUs and memories as controller does.
- Easy to connect with third-party Grafana instance. For example, Grafana from cloud providers.
##### Cons
- Cannot persist data outside Grafana storage. Once grafana is broken, the CustomResource will be unavailable as well.
The main drawback for vela-prism compared to operator pattern is that it cannot persist configurations. However, this can be solved through using KubeVela application to manage those configurations.
For example, you can write KubeVela applications to hold the dashboard configurations, instead of create another separate CustomResource. With KubeVela application, leverage GitOps is also possible.

View File

@ -3,7 +3,7 @@ kind: APIService
metadata:
name: v1alpha1.prism.oam.dev
labels:
api: kuebvela-vela-prism
api: kubevela-vela-prism
apiserver: "true"
{{- include "vela-prism.labels" . | nindent 4 }}
spec:
@ -18,4 +18,50 @@ spec:
insecureSkipTLSVerify: {{ not .Values.secureTLS.enabled }}
{{ if .Values.secureTLS.enabled }}
caBundle: Cg==
{{ end }}
{{ end }}
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.o11y.prism.oam.dev
labels:
api: kubevela-vela-prism
apiserver: "true"
{{- include "vela-prism.labels" . | nindent 4 }}
spec:
version: v1alpha1
group: o11y.prism.oam.dev
groupPriorityMinimum: 2000
service:
name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
port: {{ .Values.port }}
versionPriority: 10
insecureSkipTLSVerify: {{ not .Values.secureTLS.enabled }}
{{ if .Values.secureTLS.enabled }}
caBundle: Cg==
{{ end }}
---
{{ if .Values.dynamicAPI.enabled }}
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.dynamic.prism.oam.dev
labels:
api: kubevela-vela-prism
apiserver: "true"
{{- include "vela-prism.labels" . | nindent 4 }}
spec:
version: v1alpha1
group: dynamic.prism.oam.dev
groupPriorityMinimum: 2000
service:
name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
port: {{ .Values.port }}
versionPriority: 10
insecureSkipTLSVerify: {{ not .Values.secureTLS.enabled }}
{{ if .Values.secureTLS.enabled }}
caBundle: Cg==
{{ end }}
{{ end }}

View File

@ -37,6 +37,20 @@ spec:
- mountPath: {{ .Values.secureTLS.certPath }}
name: tls-cert-vol
readOnly: true
livenessProbe:
httpGet:
scheme: HTTPS
path: /livez
port: {{ .Values.port }}
initialDelaySeconds: 1
periodSeconds: 10
readinessProbe:
httpGet:
scheme: HTTPS
path: /readyz
port: {{ .Values.port }}
initialDelaySeconds: 1
periodSeconds: 10
{{- end }}
{{ if .Values.secureTLS.enabled }}
volumes:

View File

@ -164,6 +164,26 @@ spec:
- --secret-namespace={{ .Release.Namespace }}
- --secret-name={{ .Release.Name }}
- --target-APIService=v1alpha1.prism.oam.dev
- name: patch-o11y
image: {{ .Values.imageRegistry }}{{ .Values.secureTLS.certPatch.image.repository }}:{{ .Values.secureTLS.certPatch.image.tag }}
imagePullPolicy: {{ .Values.secureTLS.certPatch.image.pullPolicy }}
command:
- /patch
args:
- --secret-namespace={{ .Release.Namespace }}
- --secret-name={{ .Release.Name }}
- --target-APIService=v1alpha1.o11y.prism.oam.dev
{{ if .Values.dynamicAPI.enabled }}
- name: patch-dynamic
image: {{ .Values.imageRegistry }}{{ .Values.secureTLS.certPatch.image.repository }}:{{ .Values.secureTLS.certPatch.image.tag }}
imagePullPolicy: {{ .Values.secureTLS.certPatch.image.pullPolicy }}
command:
- /patch
args:
- --secret-namespace={{ .Release.Namespace }}
- --secret-name={{ .Release.Name }}
- --target-APIService=v1alpha1.dynamic.prism.oam.dev
{{ end }}
restartPolicy: OnFailure
serviceAccountName: {{ .Release.Name }}-certpatch
securityContext:

View File

@ -30,10 +30,15 @@ rules:
verbs: ["*"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
verbs: ["get", "watch", "list", "create", "update", "delete"]
- apiGroups: ["cluster.open-cluster-management.io"]
resources: ["managedclusters"]
verbs: ["get", "watch", "list"]
{{ if .Values.dynamicAPI.enabled }}
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "create", "patch", "update", "delete", "watch"]
{{ end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -40,5 +40,8 @@ secureTLS:
certPatch:
image:
repository: oamdev/cluster-gateway
tag: v1.3.2
pullPolicy: IfNotPresent
tag: v1.4.0
pullPolicy: IfNotPresent
dynamicAPI:
enabled: true

View File

@ -1,8 +1,9 @@
ARG BASE_IMAGE
# Build the manager binary
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.18-alpine as builder
ARG GOPROXY
ENV GOPROXY=${GOPROXY:-https://goproxy.cn}
FROM golang:1.19-alpine as builder
ARG OS
ARG ARCH
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
@ -15,31 +16,20 @@ RUN go mod download
COPY cmd/apiserver/main.go cmd/apiserver/main.go
COPY pkg/ pkg/
# Build
ARG TARGETARCH
RUN CGO_ENABLED=0 \
GOOS=${OS} \
GOARCH=${ARCH} \
go build \
-a -ldflags "-s -w" \
-o vela-prism \
cmd/apiserver/main.go
RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \
go build -a -ldflags "-s -w" \
-o vela-prism-${TARGETARCH} cmd/apiserver/main.go
# Before copying the Go binary directly to the final image,
# add them to the intermdediate upx image
FROM gruebel/upx:latest as upx
ARG TARGETARCH
COPY --from=builder /workspace/vela-prism-${TARGETARCH} /workspace/vela-prism-${TARGETARCH}
# Compress the binary and copy it to final image
RUN upx --best --lzma -o /workspace/vela-prism-${TARGETARCH}-upx /workspace/vela-prism-${TARGETARCH}
# Overwrite `BASE_IMAGE` by passing `--build-arg=BASE_IMAGE=gcr.io/distroless/static:nonroot`
FROM ${BASE_IMAGE:-alpine:3.15}
FROM alpine:3.21.0
# This is required by daemon connnecting with cri
RUN apk add --no-cache ca-certificates bash expat
RUN apk add curl
WORKDIR /
ARG TARGETARCH
COPY --from=upx /workspace/vela-prism-${TARGETARCH}-upx /usr/local/bin/vela-prism
COPY --from=builder /workspace/vela-prism /usr/local/bin/vela-prism
CMD ["vela-prism"]

View File

@ -17,13 +17,21 @@ limitations under the License.
package main
import (
"k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/apiserver-runtime/pkg/builder"
cueserver "github.com/kubevela/pkg/cue/server"
apiserveroptions "github.com/kubevela/pkg/util/apiserver/options"
"github.com/kubevela/pkg/util/log"
"github.com/kubevela/pkg/util/singleton"
apprtv1alpha1 "github.com/kubevela/prism/pkg/apis/applicationresourcetracker/v1alpha1"
clusterv1alpha1 "github.com/kubevela/prism/pkg/apis/cluster/v1alpha1"
"github.com/kubevela/prism/pkg/util/log"
"github.com/kubevela/prism/pkg/util/singleton"
o11yconfig "github.com/kubevela/prism/pkg/apis/o11y/config"
grafanav1alpha1 "github.com/kubevela/prism/pkg/apis/o11y/grafana/v1alpha1"
grafanadashboardv1alpha1 "github.com/kubevela/prism/pkg/apis/o11y/grafanadashboard/v1alpha1"
grafanadatasourcev1alpha1 "github.com/kubevela/prism/pkg/apis/o11y/grafanadatasource/v1alpha1"
apiserver "github.com/kubevela/prism/pkg/dynamicapiserver"
)
func main() {
@ -34,14 +42,17 @@ func main() {
WithoutEtcd().
WithResource(&apprtv1alpha1.ApplicationResourceTracker{}).
WithResource(&clusterv1alpha1.Cluster{}).
WithPostStartHook("init-master-loopback-client", singleton.InitLoopbackClient).
WithResource(&grafanav1alpha1.Grafana{}).
WithResource(&grafanadatasourcev1alpha1.GrafanaDatasource{}).
WithResource(&grafanadashboardv1alpha1.GrafanaDashboard{}).
WithConfigFns(apiserveroptions.WrapConfig, singleton.InitServerConfig).
WithServerFns(cueserver.RegisterGenericAPIServer, singleton.InitGenericAPIServer).
WithPostStartHook("start-dynamic-server", apiserver.StartDefaultDynamicAPIServer).
Build()
if err != nil {
klog.Fatal(err)
}
runtime.Must(err)
log.AddLogFlags(cmd)
apiserveroptions.AddServerRunFlags(cmd.Flags())
clusterv1alpha1.AddClusterFlags(cmd.Flags())
if err = cmd.Execute(); err != nil {
klog.Fatal(err)
}
o11yconfig.AddObservabilityFlags(cmd.Flags())
runtime.Must(cmd.Execute())
}

View File

@ -9,3 +9,4 @@ coverage:
ignore:
- "**/zz_generated.deepcopy.go"
- "cmd/apiserver/*"
- "test/**"

174
go.mod
View File

@ -1,118 +1,134 @@
module github.com/kubevela/prism
go 1.18
go 1.19
require (
github.com/oam-dev/cluster-gateway v1.4.0
github.com/onsi/ginkgo/v2 v2.1.4
github.com/onsi/gomega v1.19.0
github.com/spf13/cobra v1.4.0
cuelang.org/go v0.5.0-beta.2.0.20230130095913-d573e0c2f041
github.com/emicklei/go-restful/v3 v3.9.0
github.com/kubevela/pkg v1.8.1-0.20230403024929-46ddc1466157
github.com/oam-dev/cluster-gateway v1.9.0-alpha.1
github.com/onsi/ginkgo/v2 v2.9.2
github.com/onsi/gomega v1.27.5
github.com/spf13/pflag v1.0.5
k8s.io/api v0.23.6
k8s.io/apimachinery v0.23.6
k8s.io/apiserver v0.23.6
k8s.io/client-go v0.23.6
k8s.io/klog/v2 v2.60.1
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
github.com/stretchr/testify v1.8.0
golang.org/x/exp v0.0.0-20221114191408-850992195362
k8s.io/api v0.26.3
k8s.io/apimachinery v0.26.3
k8s.io/apiserver v0.26.3
k8s.io/client-go v0.26.3
k8s.io/klog/v2 v2.80.1
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
open-cluster-management.io/api v0.7.0
sigs.k8s.io/apiserver-runtime v1.1.1
sigs.k8s.io/controller-runtime v0.11.2
sigs.k8s.io/controller-tools v0.6.2
sigs.k8s.io/apiserver-runtime v1.1.2-0.20221118041430-0a6394f6dda3
sigs.k8s.io/controller-runtime v0.14.5
sigs.k8s.io/controller-tools v0.11.3
)
require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cockroachdb/apd/v2 v2.0.2 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/go-logr/zapr v1.2.0 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gobuffalo/flect v0.2.3 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobuffalo/flect v0.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/cel-go v0.12.6 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/openshift/library-go v0.0.0-20220112153822-ac82336bd076 // indirect
github.com/openshift/library-go v0.0.0-20230327085348-8477ec72b725 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.28.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
go.opentelemetry.io/contrib v0.20.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect
go.opentelemetry.io/otel v0.20.0 // indirect
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
go.opentelemetry.io/otel/metric v0.20.0 // indirect
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
go.opentelemetry.io/otel/trace v0.20.0 // indirect
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.5 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.5 // indirect
go.etcd.io/etcd/client/v3 v3.5.5 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.35.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.35.0 // indirect
go.opentelemetry.io/otel v1.10.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0 // indirect
go.opentelemetry.io/otel/metric v0.31.0 // indirect
go.opentelemetry.io/otel/sdk v1.10.0 // indirect
go.opentelemetry.io/otel/trace v1.10.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 // indirect
google.golang.org/grpc v1.42.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiextensions-apiserver v0.23.5 // indirect
k8s.io/component-base v0.23.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.26.3 // indirect
k8s.io/component-base v0.26.3 // indirect
k8s.io/klog v1.0.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/kms v0.26.3 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
sigs.k8s.io/apiserver-network-proxy v0.0.30 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.36 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace sigs.k8s.io/apiserver-network-proxy/konnectivity-client => sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.24
replace (
cloud.google.com/go => cloud.google.com/go v0.100.2
sigs.k8s.io/apiserver-network-proxy/konnectivity-client => sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.36
)

1016
go.sum

File diff suppressed because it is too large Load Diff

BIN
hack/prism-grafana-arch.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -18,47 +18,14 @@ package v1alpha1
import (
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"github.com/kubevela/prism/pkg/util/singleton"
_ "github.com/kubevela/prism/test/bootstrap"
)
var testEnv *envtest.Environment
func TestApplicationResourceTracker(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "ApplicationResourceTracker Extension API Test")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
By("Bootstrapping Test Environment")
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute,
ControlPlaneStopTimeout: time.Minute,
Scheme: scheme.Scheme,
CRDDirectoryPaths: []string{"../../../../test/testdata/crds"},
UseExistingCluster: pointer.Bool(false),
}
cfg, err := testEnv.Start()
Ω(err).To(Succeed())
singleton.SetKubeConfig(cfg)
k8sClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme})
Ω(err).To(Succeed())
singleton.SetKubeClient(k8sClient)
})
var _ = AfterSuite(func() {
By("Tearing Down the Test Environment")
Ω(testEnv.Stop()).To(Succeed())
})

View File

@ -31,7 +31,7 @@ import (
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubevela/prism/pkg/util/singleton"
"github.com/kubevela/pkg/util/singleton"
)
// ApplicationResourceTracker is an extension model for ResourceTracker
@ -69,6 +69,9 @@ func (in *ApplicationResourceTracker) New() runtime.Object {
return &ApplicationResourceTracker{}
}
// Destroy .
func (in *ApplicationResourceTracker) Destroy() {}
// NewList return a new list instance of the resource
func (in *ApplicationResourceTracker) NewList() runtime.Object {
return &ApplicationResourceTrackerList{}
@ -94,7 +97,7 @@ func (in *ApplicationResourceTracker) Get(ctx context.Context, name string, opti
rt := &unstructured.Unstructured{}
rt.SetGroupVersionKind(ResourceTrackerGroupVersionKind)
ns := request.NamespaceValue(ctx)
if err := singleton.GetKubeClient().Get(ctx, types.NamespacedName{Name: name + "-" + ns}, rt); err != nil {
if err := singleton.KubeClient.Get().Get(ctx, types.NamespacedName{Name: name + "-" + ns}, rt); err != nil {
return nil, err
}
return NewApplicationResourceTrackerFromResourceTracker(rt)
@ -109,7 +112,7 @@ func (in *ApplicationResourceTracker) List(ctx context.Context, options *metaint
if options != nil {
sel.selector = options.LabelSelector
}
if err := singleton.GetKubeClient().List(ctx, rts, sel); err != nil {
if err := singleton.KubeClient.Get().List(ctx, rts, sel); err != nil {
return nil, err
}
appRts := &ApplicationResourceTrackerList{}

View File

@ -27,7 +27,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/kubevela/prism/pkg/util/singleton"
"github.com/kubevela/pkg/util/singleton"
)
var _ = Describe("Test ApplicationResourceTracker API", func() {
@ -55,7 +55,7 @@ var _ = Describe("Test ApplicationResourceTracker API", func() {
labelAppNamespace: ns,
"key": val,
})
Ω(singleton.GetKubeClient().Create(ctx, rt)).To(Succeed())
Ω(singleton.KubeClient.Get().Create(ctx, rt)).To(Succeed())
return rt
}
createRt("app-1", "example", "x")

View File

@ -29,8 +29,11 @@ import (
"k8s.io/apimachinery/pkg/selection"
apitypes "k8s.io/apimachinery/pkg/types"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/utils/strings/slices"
ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubevela/pkg/util/apiserver"
)
// ClusterClient client for reading cluster information
@ -91,16 +94,9 @@ func (c *clusterClient) Get(ctx context.Context, name string) (*Cluster, error)
}
func (c *clusterClient) List(ctx context.Context, options ...client.ListOption) (*ClusterList, error) {
opts := &client.ListOptions{}
for _, opt := range options {
opt.ApplyToList(opts)
}
clusters := &ClusterList{}
if opts.LabelSelector == nil || opts.LabelSelector.Empty() {
opts.LabelSelector = labels.NewSelector()
local := NewLocalCluster()
clusters.Items = []Cluster{*local}
}
opts := apiserver.NewListOptions(options...)
local := NewLocalCluster()
clusters := &ClusterList{Items: []Cluster{*local}}
secrets := &corev1.SecretList{}
err := c.Client.List(ctx, secrets, clusterSelector{Selector: opts.LabelSelector, RequireCredentialType: true})
@ -114,7 +110,7 @@ func (c *clusterClient) List(ctx context.Context, options ...client.ListOption)
}
managedClusters := &ocmclusterv1.ManagedClusterList{}
err = c.Client.List(ctx, managedClusters, clusterSelector{Selector: opts.LabelSelector, RequireCredentialType: false})
err = c.Client.List(ctx, managedClusters, clusterSelector{Selector: opts.LabelSelector, RequireCredentialType: false, IgnoreNamespace: true})
if err != nil && !meta.IsNoMatchError(err) && !runtime.IsNotRegisteredError(err) {
return nil, err
}
@ -125,6 +121,17 @@ func (c *clusterClient) List(ctx context.Context, options ...client.ListOption)
}
}
}
// filter clusters
var items []Cluster
for _, cluster := range clusters.Items {
if opts.LabelSelector == nil || opts.LabelSelector.Matches(labels.Set(cluster.GetLabels())) {
items = append(items, cluster)
}
}
clusters.Items = items
// sort clusters
sort.Slice(clusters.Items, func(i, j int) bool {
if clusters.Items[i].Name == ClusterLocalName {
return true
@ -141,14 +148,25 @@ func (c *clusterClient) List(ctx context.Context, options ...client.ListOption)
type clusterSelector struct {
Selector labels.Selector
RequireCredentialType bool
IgnoreNamespace bool
}
// ApplyToList applies this configuration to the given list options.
func (m clusterSelector) ApplyToList(opts *client.ListOptions) {
opts.LabelSelector = m.Selector
opts.LabelSelector = labels.NewSelector()
if m.Selector != nil {
requirements, _ := m.Selector.Requirements()
for _, r := range requirements {
if !slices.Contains([]string{LabelClusterControlPlane}, r.Key()) {
opts.LabelSelector = opts.LabelSelector.Add(r)
}
}
}
if m.RequireCredentialType {
r, _ := labels.NewRequirement(clustergatewaycommon.LabelKeyClusterCredentialType, selection.Exists, nil)
opts.LabelSelector = opts.LabelSelector.Add(*r)
}
opts.Namespace = StorageNamespace
if !m.IgnoreNamespace {
opts.Namespace = StorageNamespace
}
}

View File

@ -36,6 +36,9 @@ const (
var (
// AnnotationClusterAlias the annotation key for cluster alias
AnnotationClusterAlias = config.MetaApiGroupName + "/cluster-alias"
// LabelClusterControlPlane identifies whether the cluster is the control plane
LabelClusterControlPlane = config.MetaApiGroupName + "/control-plane"
)
// StorageNamespace refers to the namespace of cluster secret, usually same as the core kubevela system namespace

View File

@ -18,20 +18,31 @@ package v1alpha1
import "errors"
type invalidClusterSecretError struct{}
type emptyCredentialTypeClusterSecretError struct{}
func (e invalidClusterSecretError) Error() string {
func (e emptyCredentialTypeClusterSecretError) Error() string {
return "secret is not a valid cluster secret, no credential type found"
}
// NewInvalidClusterSecretError create an invalid cluster secret error
func NewInvalidClusterSecretError() error {
return invalidClusterSecretError{}
// NewEmptyCredentialTypeClusterSecretError create an invalid cluster secret error due to empty credential type
func NewEmptyCredentialTypeClusterSecretError() error {
return emptyCredentialTypeClusterSecretError{}
}
type emptyEndpointClusterSecretError struct{}
func (e emptyEndpointClusterSecretError) Error() string {
return "secret is not a valid cluster secret, no credential type found"
}
// NewEmptyEndpointClusterSecretError create an invalid cluster secret error due to empty endpoint
func NewEmptyEndpointClusterSecretError() error {
return emptyEndpointClusterSecretError{}
}
// IsInvalidClusterSecretError check if an error is an invalid cluster secret error
func IsInvalidClusterSecretError(err error) bool {
return errors.As(err, &invalidClusterSecretError{})
return errors.As(err, &emptyCredentialTypeClusterSecretError{}) || errors.As(err, &emptyEndpointClusterSecretError{})
}
type invalidManagedClusterError struct{}

View File

@ -29,9 +29,9 @@ import (
func (in *Cluster) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
switch obj := object.(type) {
case *Cluster:
return printClusterGateway(obj), nil
return printCluster(obj), nil
case *ClusterList:
return printClusterGatewayList(obj), nil
return printClusterList(obj), nil
default:
return nil, fmt.Errorf("unknown type %T", object)
}
@ -49,14 +49,14 @@ var (
}
)
func printClusterGateway(in *Cluster) *metav1.Table {
func printCluster(in *Cluster) *metav1.Table {
return &metav1.Table{
ColumnDefinitions: definitions,
Rows: []metav1.TableRow{printClusterRow(in)},
}
}
func printClusterGatewayList(in *ClusterList) *metav1.Table {
func printClusterList(in *ClusterList) *metav1.Table {
t := &metav1.Table{
ColumnDefinitions: definitions,
}

View File

@ -18,6 +18,7 @@ package v1alpha1
import (
"context"
"fmt"
"strings"
clustergatewayv1alpha1 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
@ -26,26 +27,22 @@ import (
corev1 "k8s.io/api/core/v1"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubevela/prism/pkg/util/singleton"
"github.com/kubevela/pkg/util/apiserver"
"github.com/kubevela/pkg/util/singleton"
)
// Get finds a resource in the storage by name and returns it.
func (in *Cluster) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return NewClusterClient(singleton.GetKubeClient()).Get(ctx, name)
return NewClusterClient(singleton.KubeClient.Get()).Get(ctx, name)
}
// List selects resources in the storage which match to the selector. 'options' can be nil.
func (in *Cluster) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
sel := labels.NewSelector()
if options != nil && options.LabelSelector != nil && !options.LabelSelector.Empty() {
sel = options.LabelSelector
}
return NewClusterClient(singleton.GetKubeClient()).List(ctx, client.MatchingLabelsSelector{Selector: sel})
return NewClusterClient(singleton.KubeClient.Get()).List(ctx, apiserver.NewMatchingLabelSelectorFromInternalVersionListOptions(options))
}
func extractLabels(labels map[string]string) map[string]string {
@ -71,6 +68,7 @@ func newCluster(obj client.Object) *Cluster {
}
cluster.Spec.Accepted = true
cluster.Spec.Endpoint = ClusterBlankEndpoint
metav1.SetMetaDataLabel(&cluster.ObjectMeta, LabelClusterControlPlane, fmt.Sprintf("%t", obj == nil))
return cluster
}
@ -89,8 +87,11 @@ func NewClusterFromSecret(secret *corev1.Secret) (*Cluster, error) {
if metav1.HasLabel(secret.ObjectMeta, clustergatewaycommon.LabelKeyClusterEndpointType) {
cluster.Spec.Endpoint = secret.GetLabels()[clustergatewaycommon.LabelKeyClusterEndpointType]
}
if cluster.Spec.Endpoint == "" {
return nil, NewEmptyEndpointClusterSecretError()
}
if !metav1.HasLabel(secret.ObjectMeta, clustergatewaycommon.LabelKeyClusterCredentialType) {
return nil, NewInvalidClusterSecretError()
return nil, NewEmptyCredentialTypeClusterSecretError()
}
cluster.Spec.CredentialType = clustergatewayv1alpha1.CredentialType(
secret.GetLabels()[clustergatewaycommon.LabelKeyClusterCredentialType])

View File

@ -18,47 +18,14 @@ package v1alpha1
import (
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"github.com/kubevela/prism/pkg/util/singleton"
_ "github.com/kubevela/prism/test/bootstrap"
)
var testEnv *envtest.Environment
func TestCluster(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cluster Extension API Test")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
By("Bootstrapping Test Environment")
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute,
ControlPlaneStopTimeout: time.Minute,
Scheme: scheme.Scheme,
CRDDirectoryPaths: []string{"../../../../test/testdata/crds"},
UseExistingCluster: pointer.Bool(false),
}
cfg, err := testEnv.Start()
Ω(err).To(Succeed())
singleton.SetKubeConfig(cfg)
k8sClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme})
Ω(err).To(Succeed())
singleton.SetKubeClient(k8sClient)
})
var _ = AfterSuite(func() {
By("Tearing Down the Test Environment")
Ω(testEnv.Stop()).To(Succeed())
})

View File

@ -69,6 +69,9 @@ func (in *Cluster) New() runtime.Object {
return &Cluster{}
}
// Destroy .
func (in *Cluster) Destroy() {}
// NewList return a new list instance of the resource
func (in *Cluster) NewList() runtime.Object {
return &ClusterList{}

View File

@ -30,7 +30,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
ocmclusterv1 "open-cluster-management.io/api/cluster/v1"
"github.com/kubevela/prism/pkg/util/singleton"
"github.com/kubevela/pkg/util/singleton"
)
var _ = Describe("Test Cluster API", func() {
@ -58,10 +58,10 @@ var _ = Describe("Test Cluster API", func() {
ctx := context.Background()
By("Create storage namespace")
Ω(singleton.GetKubeClient().Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: StorageNamespace}})).To(Succeed())
Ω(singleton.KubeClient.Get().Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: StorageNamespace}})).To(Succeed())
By("Create cluster secret")
Ω(singleton.GetKubeClient().Create(ctx, &v1.Secret{
Ω(singleton.KubeClient.Get().Create(ctx, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cluster",
Namespace: StorageNamespace,
@ -73,11 +73,21 @@ var _ = Describe("Test Cluster API", func() {
Annotations: map[string]string{AnnotationClusterAlias: "test-cluster-alias"},
},
})).To(Succeed())
Ω(singleton.GetKubeClient().Create(ctx, &v1.Secret{
Ω(singleton.KubeClient.Get().Create(ctx, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-invalid",
Namespace: StorageNamespace,
},
Data: map[string][]byte{"endpoint": []byte("127.0.0.1:6443")},
})).To(Succeed())
Ω(singleton.KubeClient.Get().Create(ctx, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "ocm-cluster",
Namespace: StorageNamespace,
Labels: map[string]string{
clustergatewaycommon.LabelKeyClusterCredentialType: string(clustergatewayv1alpha1.CredentialTypeX509Certificate),
},
},
})).To(Succeed())
By("Test get cluster from cluster secret")
@ -93,13 +103,13 @@ var _ = Describe("Test Cluster API", func() {
Ω(err).To(Satisfy(IsInvalidClusterSecretError))
By("Create OCM ManagedCluster")
Ω(singleton.GetKubeClient().Create(ctx, &ocmclusterv1.ManagedCluster{
Ω(singleton.KubeClient.Get().Create(ctx, &ocmclusterv1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "ocm-bad-cluster",
Namespace: StorageNamespace,
},
})).To(Succeed())
Ω(singleton.GetKubeClient().Create(ctx, &ocmclusterv1.ManagedCluster{
Ω(singleton.KubeClient.Get().Create(ctx, &ocmclusterv1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "ocm-cluster",
Namespace: StorageNamespace,
@ -109,7 +119,7 @@ var _ = Describe("Test Cluster API", func() {
ManagedClusterClientConfigs: []ocmclusterv1.ClientConfig{{URL: "test-url"}},
},
})).To(Succeed())
Ω(singleton.GetKubeClient().Create(ctx, &ocmclusterv1.ManagedCluster{
Ω(singleton.KubeClient.Get().Create(ctx, &ocmclusterv1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cluster",
Namespace: StorageNamespace,
@ -159,6 +169,27 @@ var _ = Describe("Test Cluster API", func() {
Expect(clusters.Items[0].Name).To(Equal("ocm-cluster"))
Expect(clusters.Items[1].Name).To(Equal("test-cluster"))
By("Test list clusters that are not control plane")
objs, err = c.List(ctx, &metainternalversion.ListOptions{LabelSelector: labels.SelectorFromSet(map[string]string{
LabelClusterControlPlane: "false",
})})
Ω(err).To(Succeed())
clusters, ok = objs.(*ClusterList)
Ω(ok).To(BeTrue())
Expect(len(clusters.Items)).To(Equal(2))
Expect(clusters.Items[0].Name).To(Equal("ocm-cluster"))
Expect(clusters.Items[1].Name).To(Equal("test-cluster"))
By("Test list clusters that is control plane")
objs, err = c.List(ctx, &metainternalversion.ListOptions{LabelSelector: labels.SelectorFromSet(map[string]string{
LabelClusterControlPlane: "true",
})})
Ω(err).To(Succeed())
clusters, ok = objs.(*ClusterList)
Ω(ok).To(BeTrue())
Expect(len(clusters.Items)).To(Equal(1))
Expect(clusters.Items[0].Name).To(Equal("local"))
By("Test print table")
_, err = c.ConvertToTable(ctx, cluster, nil)
Ω(err).To(Succeed())

View File

@ -0,0 +1,197 @@
/*
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 dynamicresource
import (
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/pkg/strings"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/kubevela/pkg/util/singleton"
)
type Typer interface {
GroupVersion() schema.GroupVersion
GroupVersionKind() schema.GroupVersionKind
GroupVersionKindList() schema.GroupVersionKind
GroupVersionResource() schema.GroupVersionResource
Kind() string
KindList() string
Resource() string
Namespaced() bool
}
type Codec interface {
Source() Typer
Target() Typer
Encode(src *unstructured.Unstructured) (*unstructured.Unstructured, error)
Decode(src *unstructured.Unstructured) (*unstructured.Unstructured, error)
}
type typer struct {
groupVersion schema.GroupVersion
kind string
resource string
kindList string
namespaced bool
}
var _ Typer = &typer{}
func (in *typer) GroupVersion() schema.GroupVersion {
return in.groupVersion
}
func (in *typer) GroupVersionKind() schema.GroupVersionKind {
return in.groupVersion.WithKind(in.kind)
}
func (in *typer) GroupVersionKindList() schema.GroupVersionKind {
return in.groupVersion.WithKind(in.kindList)
}
func (in *typer) GroupVersionResource() schema.GroupVersionResource {
return in.groupVersion.WithResource(in.resource)
}
func (in *typer) Kind() string {
return in.kind
}
func (in *typer) KindList() string {
return in.kindList
}
func (in *typer) Resource() string {
return in.resource
}
func (in *typer) Namespaced() bool {
return in.namespaced
}
func NewDefaultTyper(apiVersion string, kind string) (Typer, error) {
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return nil, err
}
resource := strings.ToLower(kind) + "s"
namespaced := true
mappings, err := singleton.RESTMapper.Get().RESTMappings(gv.WithKind(kind).GroupKind(), gv.Version)
if err == nil && len(mappings) > 0 {
resource = mappings[0].Resource.Resource
namespaced = mappings[0].Scope.Name() == meta.RESTScopeNameNamespace
}
return &typer{
groupVersion: gv,
kind: kind,
resource: resource,
kindList: kind + "List",
namespaced: namespaced,
}, nil
}
const (
templateParameterKey = "parameter"
templateOutputKey = "output"
)
type templateCodec struct {
source, target Typer
encodeTemplate string
decodeTemplate string
}
var _ Codec = &templateCodec{}
func (in *templateCodec) Source() Typer {
return in.source
}
func (in *templateCodec) Target() Typer {
return in.target
}
func (in *templateCodec) Encode(source *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return in.convert(source, in.encodeTemplate, in.target)
}
func (in *templateCodec) Decode(target *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return in.convert(target, in.decodeTemplate, in.source)
}
func (in *templateCodec) convert(src *unstructured.Unstructured, template string, dest Typer) (*unstructured.Unstructured, error) {
bs, err := src.MarshalJSON()
if err != nil {
return nil, err
}
if template != "" {
ctx := cuecontext.New()
param := ctx.CompileBytes(bs)
val := ctx.CompileString(template).
FillPath(cue.ParsePath(templateParameterKey), param).
LookupPath(cue.ParsePath(templateOutputKey))
if err = val.Err(); err != nil {
return nil, err
}
if bs, err = val.MarshalJSON(); err != nil {
return nil, err
}
}
out := &unstructured.Unstructured{}
if err = out.UnmarshalJSON(bs); err != nil {
return nil, err
}
out.SetGroupVersionKind(dest.GroupVersionKind())
return out, nil
}
func (in *templateCodec) loadTyper(template string, path string) (Typer, error) {
templateVal := cuecontext.New().CompileString(template).Value()
if err := templateVal.Err(); err != nil {
return nil, err
}
apiVersion, err := templateVal.LookupPath(cue.ParsePath(path + ".apiVersion")).String()
if err != nil {
return nil, err
}
kind, err := templateVal.LookupPath(cue.ParsePath(path + ".kind")).String()
if err != nil {
return nil, err
}
return NewDefaultTyper(apiVersion, kind)
}
func NewTemplateCodec(encodeTemplate, decodeTemplate string) (Codec, error) {
var err error
codec := &templateCodec{
encodeTemplate: encodeTemplate,
decodeTemplate: decodeTemplate,
}
codec.source, err = codec.loadTyper(encodeTemplate, templateParameterKey)
if err != nil {
return nil, err
}
codec.target, err = codec.loadTyper(decodeTemplate, templateParameterKey)
if err != nil {
return nil, err
}
return codec, nil
}

View File

@ -0,0 +1,82 @@
/*
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 dynamicresource_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/kubevela/prism/pkg/apis/dynamicresource"
)
var _ = Describe("Test codec", func() {
It("Test typer", func() {
_, err := dynamicresource.NewDefaultTyper("a/b/c", "")
Ω(err).NotTo(Succeed())
typer, err := dynamicresource.NewDefaultTyper("test.oam.dev/v1beta1", "Tester")
Ω(err).To(Succeed())
gv := schema.GroupVersion{Group: "test.oam.dev", Version: "v1beta1"}
Ω(typer.GroupVersion()).To(Equal(gv))
Ω(typer.GroupVersionKind()).To(Equal(gv.WithKind("Tester")))
Ω(typer.GroupVersionKindList()).To(Equal(gv.WithKind("TesterList")))
Ω(typer.GroupVersionResource()).To(Equal(gv.WithResource("testers")))
Ω(typer.Kind()).To(Equal("Tester"))
Ω(typer.KindList()).To(Equal("TesterList"))
Ω(typer.Resource()).To(Equal("testers"))
})
It("Test template codec", func() {
By("Normal codec")
codec, err := dynamicresource.NewTemplateCodec(encoderTemplate, decoderTemplate)
Ω(err).To(Succeed())
sourceGVK := schema.GroupVersionKind{Group: "test.oam.dev", Version: "v1alpha2", Kind: "Tester"}
targetGVK := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}
Ω(codec.Source().GroupVersionKind()).To(Equal(sourceGVK))
Ω(codec.Target().GroupVersionKind()).To(Equal(targetGVK))
By("Normal codec good encode and decode")
src := &unstructured.Unstructured{}
src.SetGroupVersionKind(sourceGVK)
tgt, err := codec.Encode(src)
Ω(err).To(Succeed())
Ω(tgt.GroupVersionKind()).To(Equal(targetGVK))
tgt = &unstructured.Unstructured{}
tgt.SetGroupVersionKind(targetGVK)
src, err = codec.Decode(tgt)
Ω(err).To(Succeed())
Ω(src.GroupVersionKind()).To(Equal(sourceGVK))
By("Normal codec bad encode")
src = &unstructured.Unstructured{}
src.SetGroupVersionKind(schema.GroupVersionKind{Group: "bad", Version: "unknown", Kind: "v0"})
_, err = codec.Encode(src)
Ω(err).NotTo(Succeed())
By("Codec with bad encoder")
_, err = dynamicresource.NewTemplateCodec(`bad-key: bad-val`, "")
Ω(err).NotTo(Succeed())
_, err = dynamicresource.NewTemplateCodec(`parameter: apiVersion: 1`, "")
Ω(err).NotTo(Succeed())
_, err = dynamicresource.NewTemplateCodec(`parameter: apiVersion: "v1"`, "")
Ω(err).NotTo(Succeed())
_, err = dynamicresource.NewTemplateCodec(encoderTemplate, "bad-good")
Ω(err).NotTo(Succeed())
})
})

View File

@ -0,0 +1,404 @@
/*
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 dynamicresource
import (
"context"
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/client-go/dynamic"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
"github.com/kubevela/pkg/util/singleton"
)
type DynamicResource struct {
Uns *unstructured.Unstructured
codec Codec
}
func (in *DynamicResource) GetNamespace() string {
return in.Uns.GetNamespace()
}
func (in *DynamicResource) SetNamespace(namespace string) {
in.Uns.SetNamespace(namespace)
}
func (in *DynamicResource) GetName() string {
return in.Uns.GetName()
}
func (in *DynamicResource) SetName(name string) {
in.Uns.SetName(name)
}
func (in *DynamicResource) GetGenerateName() string {
return in.Uns.GetGenerateName()
}
func (in *DynamicResource) SetGenerateName(name string) {
in.Uns.SetGenerateName(name)
}
func (in *DynamicResource) GetUID() types.UID {
return in.Uns.GetUID()
}
func (in *DynamicResource) SetUID(uid types.UID) {
in.Uns.SetUID(uid)
}
func (in *DynamicResource) GetResourceVersion() string {
return in.Uns.GetResourceVersion()
}
func (in *DynamicResource) SetResourceVersion(version string) {
in.Uns.SetResourceVersion(version)
}
func (in *DynamicResource) GetGeneration() int64 {
return in.Uns.GetGeneration()
}
func (in *DynamicResource) SetGeneration(generation int64) {
in.Uns.SetGeneration(generation)
}
func (in *DynamicResource) GetSelfLink() string {
return in.Uns.GetSelfLink()
}
func (in *DynamicResource) SetSelfLink(selfLink string) {
in.Uns.SetSelfLink(selfLink)
}
func (in *DynamicResource) GetCreationTimestamp() metav1.Time {
return in.Uns.GetCreationTimestamp()
}
func (in *DynamicResource) SetCreationTimestamp(timestamp metav1.Time) {
in.Uns.SetCreationTimestamp(timestamp)
}
func (in *DynamicResource) GetDeletionTimestamp() *metav1.Time {
return in.Uns.GetDeletionTimestamp()
}
func (in *DynamicResource) SetDeletionTimestamp(timestamp *metav1.Time) {
in.Uns.SetDeletionTimestamp(timestamp)
}
func (in *DynamicResource) GetDeletionGracePeriodSeconds() *int64 {
return in.Uns.GetDeletionGracePeriodSeconds()
}
func (in *DynamicResource) SetDeletionGracePeriodSeconds(i *int64) {
in.Uns.SetDeletionGracePeriodSeconds(i)
}
func (in *DynamicResource) GetLabels() map[string]string {
return in.Uns.GetLabels()
}
func (in *DynamicResource) SetLabels(labels map[string]string) {
in.Uns.SetLabels(labels)
}
func (in *DynamicResource) GetAnnotations() map[string]string {
return in.Uns.GetAnnotations()
}
func (in *DynamicResource) SetAnnotations(annotations map[string]string) {
in.Uns.SetAnnotations(annotations)
}
func (in *DynamicResource) GetFinalizers() []string {
return in.Uns.GetFinalizers()
}
func (in *DynamicResource) SetFinalizers(finalizers []string) {
in.Uns.SetFinalizers(finalizers)
}
func (in *DynamicResource) GetOwnerReferences() []metav1.OwnerReference {
return in.Uns.GetOwnerReferences()
}
func (in *DynamicResource) SetOwnerReferences(references []metav1.OwnerReference) {
in.Uns.SetOwnerReferences(references)
}
func (in *DynamicResource) GetManagedFields() []metav1.ManagedFieldsEntry {
return in.Uns.GetManagedFields()
}
func (in *DynamicResource) SetManagedFields(managedFields []metav1.ManagedFieldsEntry) {
in.Uns.SetManagedFields(managedFields)
}
var _ runtime.Object = &DynamicResource{}
var _ resource.Object = &DynamicResource{}
var _ rest.Storage = &DynamicResource{}
var _ rest.Getter = &DynamicResource{}
var _ rest.CreaterUpdater = &DynamicResource{}
var _ rest.Patcher = &DynamicResource{}
var _ rest.GracefulDeleter = &DynamicResource{}
var _ rest.Lister = &DynamicResource{}
var _ rest.GroupVersionKindProvider = &DynamicResource{}
var _ metav1.Object = &DynamicResource{}
type dynamicResourceObjectKind struct {
Typer
}
func (in *dynamicResourceObjectKind) SetGroupVersionKind(kind schema.GroupVersionKind) {}
func (in *dynamicResourceObjectKind) GroupVersionKind() schema.GroupVersionKind {
if in.Typer != nil {
return in.Typer.GroupVersionKind()
}
return schema.GroupVersionKind{}
}
var _ schema.ObjectKind = &dynamicResourceObjectKind{}
func (in *DynamicResource) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind {
return containingGV.WithKind(in.codec.Source().Kind())
}
func (in *DynamicResource) GroupVersionResource() schema.GroupVersionResource {
return in.codec.Source().GroupVersionResource()
}
func (in *DynamicResource) GetObjectKind() schema.ObjectKind {
objKind := &dynamicResourceObjectKind{nil}
if in.codec != nil {
objKind.Typer = in.codec.Source()
}
return objKind
}
func (in *DynamicResource) GetObjectMeta() *metav1.ObjectMeta {
return &metav1.ObjectMeta{
Name: in.Uns.GetName(),
GenerateName: in.Uns.GetGenerateName(),
Namespace: in.Uns.GetNamespace(),
UID: in.Uns.GetUID(),
ResourceVersion: in.Uns.GetResourceVersion(),
Generation: in.Uns.GetGeneration(),
CreationTimestamp: in.Uns.GetCreationTimestamp(),
DeletionTimestamp: in.Uns.GetDeletionTimestamp(),
DeletionGracePeriodSeconds: in.Uns.GetDeletionGracePeriodSeconds(),
Labels: in.Uns.GetLabels(),
Annotations: in.Uns.GetAnnotations(),
OwnerReferences: in.Uns.GetOwnerReferences(),
Finalizers: in.Uns.GetFinalizers(),
ManagedFields: in.Uns.GetManagedFields(),
}
}
func (in *DynamicResource) NamespaceScoped() bool {
return in.codec.Source().Namespaced()
}
func (in *DynamicResource) New() runtime.Object {
dr := &DynamicResource{
Uns: &unstructured.Unstructured{},
codec: in.codec,
}
if dr.codec != nil {
dr.Uns.SetGroupVersionKind(dr.codec.Source().GroupVersionKind())
}
return dr
}
func (in *DynamicResource) NewList() runtime.Object {
drs := &DynamicResourceList{
UnsList: &unstructured.UnstructuredList{},
codec: in.codec,
}
if drs.codec != nil {
drs.UnsList.SetGroupVersionKind(drs.codec.Source().GroupVersionKindList())
}
return drs
}
func (in *DynamicResource) Destroy() {}
func (in *DynamicResource) GetGroupVersionResource() schema.GroupVersionResource {
return in.codec.Source().GroupVersionResource()
}
func (in *DynamicResource) GetGroupVersionKind() schema.GroupVersionKind {
return in.codec.Source().GroupVersionKind()
}
func (in *DynamicResource) GetGroupVersion() schema.GroupVersion {
return in.codec.Source().GroupVersion()
}
func (in *DynamicResource) IsStorageVersion() bool {
return true
}
func (in *DynamicResource) DeepCopyObject() runtime.Object {
return &DynamicResource{
Uns: in.Uns.DeepCopy(),
codec: in.codec,
}
}
func (in *DynamicResource) resourceInterface(ctx context.Context) dynamic.ResourceInterface {
var ri dynamic.ResourceInterface = singleton.DynamicClient.Get().Resource(in.codec.Target().GroupVersionResource())
if in.codec.Target().Namespaced() {
ri = ri.(dynamic.NamespaceableResourceInterface).Namespace(request.NamespaceValue(ctx))
}
return ri
}
func (in *DynamicResource) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
obj, err := in.resourceInterface(ctx).Get(ctx, name, *options)
if err != nil {
return nil, err
}
decoded, err := in.codec.Decode(obj)
return decoded, err
}
func (in *DynamicResource) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
o := obj.(*DynamicResource)
encoded, err := in.codec.Encode(o.Uns)
if err != nil {
return nil, err
}
created, err := in.resourceInterface(ctx).Create(ctx, encoded, *options)
if err != nil {
return nil, err
}
decoded, err := in.codec.Decode(created)
return decoded, err
}
func (in *DynamicResource) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
decoded, err := in.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
updated, err := objInfo.UpdatedObject(ctx, decoded)
if err != nil {
return nil, false, err
}
encoded, err := in.codec.Encode(updated.(*DynamicResource).Uns)
if err != nil {
return nil, false, err
}
encoded, err = in.resourceInterface(ctx).Update(ctx, encoded, *options)
if err != nil {
return nil, false, err
}
decoded, err = in.codec.Decode(encoded)
return decoded, false, err
}
func (in *DynamicResource) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
obj, err := in.resourceInterface(ctx).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, false, err
}
if err = in.resourceInterface(ctx).Delete(ctx, name, *options); err != nil {
return nil, false, err
}
decoded, err := in.codec.Decode(obj)
return decoded, false, err
}
func (in *DynamicResource) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
v1Options := &metav1.ListOptions{}
if err := internalversion.Convert_internalversion_ListOptions_To_v1_ListOptions(options, v1Options, nil); err != nil {
return nil, err
}
objs, err := in.resourceInterface(ctx).List(ctx, *v1Options)
if err != nil {
return nil, err
}
decoded := &unstructured.UnstructuredList{}
decoded.SetGroupVersionKind(in.codec.Source().GroupVersionKindList())
for i := range objs.Items {
d, err := in.codec.Decode(&objs.Items[i])
if err != nil {
return nil, err
}
decoded.Items = append(decoded.Items, *d)
}
return decoded, nil
}
func (in *DynamicResource) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
// TODO
t := &metav1.Table{
ColumnDefinitions: []metav1.TableColumnDefinition{{
Name: "Name",
Type: "string",
}, {
Name: "CreateAt",
Type: "dateTime",
}},
Rows: []metav1.TableRow{},
}
var dat []*unstructured.Unstructured
switch obj := object.(type) {
case *unstructured.Unstructured:
dat = append(dat, obj)
case *unstructured.UnstructuredList:
for i := range obj.Items {
dat = append(dat, &obj.Items[i])
}
default:
return nil, fmt.Errorf("unknown type %T", object)
}
for _, u := range dat {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: u},
}
row.Cells = []interface{}{u.GetName(), u.GetCreationTimestamp()}
t.Rows = append(t.Rows, row)
}
return t, nil
}
func (in *DynamicResource) MarshalJSON() ([]byte, error) {
return json.Marshal(in.Uns)
}
func (in *DynamicResource) UnmarshalJSON(bs []byte) error {
in.Uns = &unstructured.Unstructured{}
return in.Uns.UnmarshalJSON(bs)
}

View File

@ -0,0 +1,58 @@
/*
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 dynamicresource
import (
"encoding/json"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type DynamicResourceList struct {
UnsList *unstructured.UnstructuredList
codec Codec
}
var _ runtime.Object = &DynamicResourceList{}
func (in *DynamicResourceList) GetObjectKind() schema.ObjectKind {
return in
}
func (in *DynamicResourceList) DeepCopyObject() runtime.Object {
return &DynamicResourceList{
UnsList: in.UnsList.DeepCopy(),
codec: in.codec,
}
}
func (in *DynamicResourceList) SetGroupVersionKind(kind schema.GroupVersionKind) {}
func (in *DynamicResourceList) GroupVersionKind() schema.GroupVersionKind {
return in.codec.Source().GroupVersionKindList()
}
func (in *DynamicResourceList) MarshalJSON() ([]byte, error) {
return json.Marshal(in.UnsList)
}
func (in *DynamicResourceList) UnmarshalJSON(bs []byte) error {
in.UnsList = &unstructured.UnstructuredList{}
return in.UnsList.UnmarshalJSON(bs)
}

View File

@ -0,0 +1,23 @@
/*
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 dynamicresource
func NewDynamicResourceWithCodec(encoderTemplate, decoderTemplate string) (obj *DynamicResource, err error) {
obj = &DynamicResource{}
obj.codec, err = NewTemplateCodec(encoderTemplate, decoderTemplate)
return obj, err
}

View File

@ -0,0 +1,31 @@
/*
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 dynamicresource_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
_ "github.com/kubevela/prism/test/bootstrap"
)
func TestDynamicResource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Test Dynamic Resource")
}

View File

@ -0,0 +1,172 @@
/*
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 dynamicresource_test
import (
"context"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/pointer"
"github.com/kubevela/prism/pkg/apis/dynamicresource"
)
const (
encoderTemplate = `
parameter: {
apiVersion: "test.oam.dev/v1alpha2"
kind: "Tester"
metadata: {...}
}
output: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: parameter.metadata
data: {}
}
`
decoderTemplate = `
parameter: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {...}
data: {...}
}
output: {
apiVersion: "test.oam.dev/v1alpha2"
kind: "Tester"
metadata: parameter.metadata
}
`
)
func newDynamicResource() (*dynamicresource.DynamicResource, error) {
return dynamicresource.NewDynamicResourceWithCodec(encoderTemplate, decoderTemplate)
}
func testSetterAndGetter[T any](setter func(T), getter func() T, val T) {
setter(val)
Ω(getter()).Should(Equal(val))
}
var _ = Describe("Test dynamic resource", func() {
It("Test types", func() {
store, err := newDynamicResource()
Ω(err).To(Succeed())
dr := store.New().(*dynamicresource.DynamicResource)
testSetterAndGetter(dr.SetNamespace, dr.GetNamespace, "ns")
testSetterAndGetter(dr.SetName, dr.GetName, "name")
testSetterAndGetter(dr.SetUID, dr.GetUID, "name")
testSetterAndGetter(dr.SetGenerateName, dr.GetGenerateName, "gn")
testSetterAndGetter(dr.SetResourceVersion, dr.GetResourceVersion, "rv")
testSetterAndGetter(dr.SetGeneration, dr.GetGeneration, 1)
testSetterAndGetter(dr.SetSelfLink, dr.GetSelfLink, "sl")
t := time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local)
testSetterAndGetter(dr.SetCreationTimestamp, dr.GetCreationTimestamp, metav1.Time{Time: t})
testSetterAndGetter(dr.SetDeletionTimestamp, dr.GetDeletionTimestamp, &metav1.Time{Time: t})
testSetterAndGetter(dr.SetDeletionGracePeriodSeconds, dr.GetDeletionGracePeriodSeconds, pointer.Int64(1))
testSetterAndGetter(dr.SetLabels, dr.GetLabels, map[string]string{"x": "y"})
testSetterAndGetter(dr.SetAnnotations, dr.GetAnnotations, map[string]string{"x": "x"})
testSetterAndGetter(dr.SetFinalizers, dr.GetFinalizers, []string{"f"})
testSetterAndGetter(dr.SetOwnerReferences, dr.GetOwnerReferences, []metav1.OwnerReference{{Kind: "k"}})
testSetterAndGetter(dr.SetManagedFields, dr.GetManagedFields, []metav1.ManagedFieldsEntry{{Manager: "m"}})
Ω(dr.GroupVersionKind(schema.GroupVersion{})).To(Equal(schema.GroupVersionKind{Kind: "Tester"}))
gv := schema.GroupVersion{Group: "test.oam.dev", Version: "v1alpha2"}
gvr := gv.WithResource("testers")
gvk := gv.WithKind("Tester")
Ω(dr.GroupVersionResource()).To(Equal(gvr))
objKind := dr.GetObjectKind()
objKind.SetGroupVersionKind(schema.GroupVersionKind{})
Ω(objKind.GroupVersionKind()).To(Equal(gvk))
Ω(dr.NamespaceScoped()).To(BeTrue())
defer dr.Destroy()
Ω(dr.GetGroupVersionResource()).To(Equal(gvr))
Ω(dr.GetGroupVersionKind()).To(Equal(gvk))
Ω(dr.GetGroupVersion()).To(Equal(gv))
Ω(dr.IsStorageVersion()).To(BeTrue())
Ω(dr.DeepCopyObject().(*dynamicresource.DynamicResource).GetObjectMeta()).To(Equal(dr.GetObjectMeta()))
bs, err := dr.MarshalJSON()
Ω(err).To(Succeed())
ndr := dr.New().(*dynamicresource.DynamicResource)
Ω(ndr.UnmarshalJSON(bs)).To(Succeed())
Ω(ndr.GetObjectMeta()).To(Equal(dr.GetObjectMeta()))
drs := dr.NewList().(*dynamicresource.DynamicResourceList)
drs.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{})
Ω(drs.GetObjectKind().GroupVersionKind()).To(Equal(gv.WithKind("TesterList")))
drs.UnsList.Items = make([]unstructured.Unstructured, 1)
Ω(len(drs.DeepCopyObject().(*dynamicresource.DynamicResourceList).UnsList.Items)).To(Equal(1))
ndrs := dr.NewList().(*dynamicresource.DynamicResourceList)
bs, err = drs.MarshalJSON()
Ω(err).To(Succeed())
Ω(ndrs.UnmarshalJSON(bs)).To(Succeed())
Ω(len(ndrs.DeepCopyObject().(*dynamicresource.DynamicResourceList).UnsList.Items)).To(Equal(1))
})
It("Test CURD API", func() {
store, err := newDynamicResource()
Ω(err).To(Succeed())
ctx := request.WithNamespace(context.Background(), "default")
By("Create")
dr := store.New().(*dynamicresource.DynamicResource)
dr.SetName("example")
dr.SetLabels(map[string]string{"key": "val"})
_, err = store.Create(ctx, dr, nil, &metav1.CreateOptions{})
Ω(err).To(Succeed())
By("Get")
obj, err := store.Get(ctx, "example", &metav1.GetOptions{})
Ω(err).To(Succeed())
uns := obj.(*unstructured.Unstructured)
Ω(uns.GetLabels()["key"]).To(Equal("val"))
_, err = store.ConvertToTable(ctx, uns, nil)
Ω(err).To(Succeed())
By("Update")
dr.SetLabels(map[string]string{"key": "value"})
_, _, err = store.Update(ctx, dr.GetName(), rest.DefaultUpdatedObjectInfo(dr), nil, nil, false, &metav1.UpdateOptions{})
Ω(err).To(Succeed())
By("List")
objs, err := store.List(ctx, &internalversion.ListOptions{})
Ω(err).To(Succeed())
unsList := objs.(*unstructured.UnstructuredList)
Ω(len(unsList.Items)).To(Equal(1))
Ω(unsList.Items[0].GetLabels()["key"]).To(Equal("value"))
_, err = store.ConvertToTable(ctx, unsList, nil)
Ω(err).To(Succeed())
By("Delete")
_, _, err = store.Delete(ctx, "example", nil, &metav1.DeleteOptions{})
Ω(err).To(Succeed())
_, err = store.Get(ctx, "example", &metav1.GetOptions{})
Ω(kerrors.IsNotFound(err)).To(BeTrue())
})
})

View File

@ -0,0 +1,28 @@
/*
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 config
import "github.com/spf13/pflag"
// ObservabilityNamespace refers to the namespace for storing secrets and configs
var ObservabilityNamespace = "o11y-system"
// AddObservabilityFlags add flags for observability api
func AddObservabilityFlags(set *pflag.FlagSet) {
set.StringVarP(&ObservabilityNamespace, "observability-namespace", "", "o11y-system",
"The namespace for storing observability secrets and configs.")
}

View File

@ -0,0 +1,94 @@
/*
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 v1alpha1
import (
"context"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubevela/pkg/util/apiserver"
"github.com/kubevela/prism/pkg/apis/o11y/config"
)
// GrafanaClient client for operate grafana
// +kubebuilder:object:generate=false
type GrafanaClient interface {
Get(ctx context.Context, name string) (*Grafana, error)
List(ctx context.Context, options ...client.ListOption) (*GrafanaList, error)
Create(ctx context.Context, grafana *Grafana) error
Update(ctx context.Context, grafana *Grafana) error
Delete(ctx context.Context, grafana *Grafana) error
}
type grafanaClient struct {
client.Client
}
// NewGrafanaClient create a client for accessing grafana
func NewGrafanaClient(cli client.Client) GrafanaClient {
return &grafanaClient{Client: cli}
}
func (c *grafanaClient) get(ctx context.Context, name string) (*corev1.Secret, error) {
secret := &corev1.Secret{}
err := c.Client.Get(ctx, types.NamespacedName{
Name: grafanaSecretNamePrefix + name,
Namespace: config.ObservabilityNamespace}, secret)
return secret, err
}
func (c *grafanaClient) Get(ctx context.Context, name string) (*Grafana, error) {
secret, err := c.get(ctx, name)
if err != nil {
return nil, err
}
return NewGrafanaFromSecret(secret)
}
func (c *grafanaClient) List(ctx context.Context, options ...client.ListOption) (*GrafanaList, error) {
opts := apiserver.NewListOptions(options...)
opts.Namespace = config.ObservabilityNamespace
secrets := &corev1.SecretList{}
if err := c.Client.List(ctx, secrets, opts); err != nil {
return nil, err
}
grafanaList := &GrafanaList{}
for _, secret := range secrets.Items {
grafana, err := NewGrafanaFromSecret(secret.DeepCopy())
if err != nil {
continue
}
grafanaList.Items = append(grafanaList.Items, *grafana)
}
return grafanaList, nil
}
func (c *grafanaClient) Create(ctx context.Context, grafana *Grafana) error {
return c.Client.Create(ctx, grafana.ToSecret())
}
func (c *grafanaClient) Update(ctx context.Context, grafana *Grafana) error {
return c.Client.Update(ctx, grafana.ToSecret())
}
func (c *grafanaClient) Delete(ctx context.Context, grafana *Grafana) error {
return c.Client.Delete(ctx, grafana.ToSecret())
}

View File

@ -0,0 +1,82 @@
/*
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 v1alpha1
import (
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/utils/pointer"
"github.com/kubevela/prism/pkg/apis/o11y/config"
)
// ToSecret convert grafana instance to underlying secret
func (in *Grafana) ToSecret() *corev1.Secret {
secret := &corev1.Secret{Data: map[string][]byte{}}
secret.ObjectMeta = in.ObjectMeta
secret.SetName(grafanaSecretNamePrefix + in.GetName())
secret.SetNamespace(config.ObservabilityNamespace)
secret.SetOwnerReferences(nil)
annotations := in.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[grafanaSecretEndpointAnnotationKey] = in.Spec.Endpoint
secret.SetAnnotations(annotations)
if in.Spec.Access.Token != nil {
secret.Data[grafanaSecretTokenKey] = []byte(*in.Spec.Access.Token)
}
if in.Spec.Access.BasicAuth != nil {
secret.Data[grafanaSecretUsernameKey] = []byte(in.Spec.Access.Username)
secret.Data[grafanaSecretPasswordKey] = []byte(in.Spec.Access.Password)
}
return secret
}
// NewGrafanaFromSecret create grafana from secret
func NewGrafanaFromSecret(secret *corev1.Secret) (*Grafana, error) {
secret = secret.DeepCopy()
grafana := &Grafana{}
grafana.ObjectMeta = secret.ObjectMeta
if !strings.HasPrefix(secret.GetName(), grafanaSecretNamePrefix) {
return nil, NewInvalidGrafanaSecretNameError()
}
grafana.SetName(strings.TrimPrefix(secret.GetName(), grafanaSecretNamePrefix))
grafana.SetNamespace("")
if annotations := secret.GetAnnotations(); annotations != nil {
grafana.Spec.Endpoint = strings.TrimSpace(annotations[grafanaSecretEndpointAnnotationKey])
delete(annotations, grafanaSecretEndpointAnnotationKey)
grafana.SetAnnotations(annotations)
}
if grafana.Spec.Endpoint == "" {
return nil, NewEmptyEndpointGrafanaSecretError()
}
if secret.Data[grafanaSecretTokenKey] != nil {
grafana.Spec.Access.Token = pointer.String(string(secret.Data[grafanaSecretTokenKey]))
}
if secret.Data[grafanaSecretUsernameKey] != nil && secret.Data[grafanaSecretPasswordKey] != nil {
grafana.Spec.Access.BasicAuth = &BasicAuth{
Username: string(secret.Data[grafanaSecretUsernameKey]),
Password: string(secret.Data[grafanaSecretPasswordKey]),
}
}
if grafana.Spec.Access.BasicAuth == nil && grafana.Spec.Access.Token == nil {
return nil, NewEmptyCredentialGrafanaSecretError()
}
return grafana, nil
}

View File

@ -0,0 +1,26 @@
/*
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.
*/
// Api versions allow the api contract for a resource to be changed while keeping
// backward compatibility by support multiple concurrent versions
// of the same resource
// Package v1alpha1 contains types required for v1alpha1
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package,register
// +k8s:defaulter-gen=TypeMeta
// +groupName=o11y.prism.oam.dev
package v1alpha1

View File

@ -0,0 +1,52 @@
/*
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 v1alpha1
import "fmt"
type invalidGrafanaSecretNameError struct{}
func (e invalidGrafanaSecretNameError) Error() string {
return fmt.Sprintf("secret is not a valid grafana secret, name should has prefix %s", grafanaSecretNamePrefix)
}
// NewInvalidGrafanaSecretNameError create an invalid grafana secret error due to invalid name
func NewInvalidGrafanaSecretNameError() error {
return invalidGrafanaSecretNameError{}
}
type emptyEndpointGrafanaSecretError struct{}
func (e emptyEndpointGrafanaSecretError) Error() string {
return fmt.Sprintf("secret is not a valid grafana secret, no endpoint (%s) found in annotation", grafanaSecretEndpointAnnotationKey)
}
// NewEmptyEndpointGrafanaSecretError create an invalid grafana secret error due to no endpoint found
func NewEmptyEndpointGrafanaSecretError() error {
return emptyEndpointGrafanaSecretError{}
}
type emptyCredentialGrafanaSecretError struct{}
func (e emptyCredentialGrafanaSecretError) Error() string {
return fmt.Sprintf("secret is not a valid grafana secret, no credential found (token or username/password should be set)")
}
// NewEmptyCredentialGrafanaSecretError create an invalid grafana secret error due to no credential found
func NewEmptyCredentialGrafanaSecretError() error {
return emptyCredentialGrafanaSecretError{}
}

View File

@ -0,0 +1,106 @@
/*
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 v1alpha1
import (
"context"
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// GrafanaCredentialType defines the credential type for grafana
type GrafanaCredentialType string
const (
// GrafanaCredentialTypeNotAvailable not available
GrafanaCredentialTypeNotAvailable GrafanaCredentialType = "NA"
// GrafanaCredentialTypeBasicAuth basic auth
GrafanaCredentialTypeBasicAuth GrafanaCredentialType = "BasicAuth"
// GrafanaCredentialTypeBearerToken bearer token
GrafanaCredentialTypeBearerToken GrafanaCredentialType = "BearerToken"
)
// GetCredentialType .
func (in *Grafana) GetCredentialType() GrafanaCredentialType {
switch {
case in.Spec.Access.Token != nil:
return GrafanaCredentialTypeBearerToken
case in.Spec.Access.BasicAuth != nil:
return GrafanaCredentialTypeBasicAuth
default:
return GrafanaCredentialTypeNotAvailable
}
}
// ConvertToTable convert resource to table
func (in *Grafana) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
switch obj := object.(type) {
case *Grafana:
return printGrafana(obj), nil
case *GrafanaList:
return printGrafanaList(obj), nil
default:
return nil, fmt.Errorf("unknown type %T", object)
}
}
var (
definitions = []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: "the name of the Grafana"},
{Name: "Endpoint", Type: "string", Description: "the endpoint"},
{Name: "Credential_Type", Type: "string", Description: "the credential type"},
{Name: "Labels", Type: "string", Description: "the labels of the Grafana", Priority: 10},
{Name: "Creation_Timestamp", Type: "dateTime", Description: "the creation timestamp of the Grafana", Priority: 10},
}
)
func printGrafana(in *Grafana) *metav1.Table {
return &metav1.Table{
ColumnDefinitions: definitions,
Rows: []metav1.TableRow{printGrafanaRow(in)},
}
}
func printGrafanaList(in *GrafanaList) *metav1.Table {
t := &metav1.Table{
ColumnDefinitions: definitions,
}
for _, c := range in.Items {
t.Rows = append(t.Rows, printGrafanaRow(c.DeepCopy()))
}
return t
}
func printGrafanaRow(c *Grafana) metav1.TableRow {
var labels []string
for k, v := range c.GetLabels() {
labels = append(labels, fmt.Sprintf("%s=%s", k, v))
}
row := metav1.TableRow{
Object: runtime.RawExtension{Object: c},
}
row.Cells = append(row.Cells,
c.Name,
c.Spec.Endpoint,
c.GetCredentialType(),
strings.Join(labels, ","),
c.GetCreationTimestamp())
return row
}

View File

@ -0,0 +1,63 @@
/*
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
)
const (
// Group the group for the apiextensions
Group = "o11y.prism.oam.dev"
// Version the version for the v1alpha1 apiextensions
Version = "v1alpha1"
)
func init() {
if err := AddToScheme(scheme.Scheme); err != nil {
klog.Fatalf("failed registering api types")
}
}
// AddToScheme add virtual cluster scheme
var AddToScheme = func(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, GroupVersion)
// +kubebuilder:scaffold:install
scheme.AddKnownTypes(GroupVersion,
&Grafana{},
&GrafanaList{},
)
return nil
}
// GroupVersion the apiextensions v1alpha1 group version
var GroupVersion = schema.GroupVersion{Group: Group, Version: Version}
var (
// GrafanaResource resource name for Grafana
GrafanaResource = "grafanas"
// GrafanaKind kind name for Grafana
GrafanaKind = "Grafana"
// GrafanaGroupResource GroupResource for Grafana
GrafanaGroupResource = schema.GroupResource{Group: Group, Resource: GrafanaResource}
// GrafanaGroupVersionKind GroupVersionKind for Grafana
GrafanaGroupVersionKind = GroupVersion.WithKind(GrafanaKind)
)

View File

@ -0,0 +1,138 @@
/*
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 v1alpha1
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strings"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
"github.com/kubevela/prism/pkg/util/subresource"
)
// DoRequest do request for the current grafana
func (in *Grafana) DoRequest(ctx context.Context, method string, path string, body io.Reader) ([]byte, int, error) {
req, err := http.NewRequestWithContext(ctx, method, strings.Trim(in.Spec.Endpoint, "/")+path, body)
if err != nil {
return nil, http.StatusInternalServerError, err
}
// set headers
switch {
case in.Spec.Access.Token != nil:
req.Header.Set("Authorization", "Bearer "+*in.Spec.Access.Token)
case in.Spec.Access.BasicAuth != nil:
req.SetBasicAuth(in.Spec.Access.Username, in.Spec.Access.Password)
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, http.StatusInternalServerError, err
}
defer func() { _ = resp.Body.Close() }()
bs, err := io.ReadAll(resp.Body)
return bs, resp.StatusCode, err
}
// GrafanaSubResourceRequest request for grafana subresources
// +kubebuilder:object:generate=false
type GrafanaSubResourceRequest struct {
resourceName *subresource.CompoundName
subResource resource.Object
method string
pathFunc func() (string, error)
bodyFunc func() ([]byte, error)
onSuccess func(respBody []byte) error
}
// NewGrafanaSubResourceRequest create request for grafana subresource
func NewGrafanaSubResourceRequest(subResource resource.Object, name string) *GrafanaSubResourceRequest {
return &GrafanaSubResourceRequest{
resourceName: subresource.NewCompoundName(name),
subResource: subResource,
}
}
func (in *GrafanaSubResourceRequest) WithMethod(method string) *GrafanaSubResourceRequest {
in.method = method
return in
}
func (in *GrafanaSubResourceRequest) WithPathFunc(pathFunc func() (string, error)) *GrafanaSubResourceRequest {
in.pathFunc = pathFunc
return in
}
func (in *GrafanaSubResourceRequest) WithBodyFunc(bodyFunc func() ([]byte, error)) *GrafanaSubResourceRequest {
in.bodyFunc = bodyFunc
return in
}
func (in *GrafanaSubResourceRequest) WithOnSuccess(onSuccess func(respBody []byte) error) *GrafanaSubResourceRequest {
in.onSuccess = onSuccess
return in
}
func (in *GrafanaSubResourceRequest) Do(ctx context.Context, cli GrafanaClient) error {
parent, err := cli.Get(ctx, in.resourceName.ParentResourceName)
if err != nil {
return err
}
var path string
if in.pathFunc != nil {
if path, err = in.pathFunc(); err != nil {
return err
}
}
var body io.Reader = nil
if in.bodyFunc != nil {
var bs []byte
if bs, err = in.bodyFunc(); err != nil {
return err
}
body = bytes.NewReader(bs)
}
respBody, statusCode, err := parent.DoRequest(ctx, in.method, path, body)
if err != nil {
return err
}
switch statusCode {
case http.StatusOK:
if in.onSuccess != nil {
return in.onSuccess(respBody)
}
case http.StatusUnauthorized:
return errors.NewUnauthorized(string(respBody))
case http.StatusForbidden:
return errors.NewForbidden(in.subResource.GetGroupVersionResource().GroupResource(), in.resourceName.String(), fmt.Errorf(string(respBody)))
case http.StatusNotFound:
return errors.NewNotFound(in.subResource.GetGroupVersionResource().GroupResource(), in.resourceName.String())
case http.StatusPreconditionFailed:
return errors.NewBadRequest(string(respBody))
default:
return fmt.Errorf("request grafana %s failed, path: %s, code: %d, detail: %s", parent.Spec.Endpoint, path, statusCode, respBody)
}
return nil
}

View File

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package log
package v1alpha1
import (
"flag"
"testing"
"github.com/spf13/cobra"
"k8s.io/klog/v2"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
_ "github.com/kubevela/prism/test/bootstrap"
)
// AddLogFlags add log flags to command
func AddLogFlags(cmd *cobra.Command) {
fs := flag.NewFlagSet("", 0)
klog.InitFlags(fs)
cmd.Flags().AddGoFlagSet(fs)
func TestGrafana(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Grafana Extension API Test")
}

View File

@ -0,0 +1,158 @@
/*
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 v1alpha1
import (
"context"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
"github.com/kubevela/pkg/util/apiserver"
"github.com/kubevela/pkg/util/singleton"
)
// Grafana defines the instance of grafana
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Grafana struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GrafanaSpec `json:"spec,omitempty"`
}
// GrafanaSpec defines the spec for grafana instance
type GrafanaSpec struct {
Endpoint string `json:"endpoint"`
Access AccessCredential `json:"access"`
}
// BasicAuth defines the basic auth credential
type BasicAuth struct {
Username string `json:"username"`
Password string `json:"password"`
}
// AccessCredential defines the access credential for the grafana api
type AccessCredential struct {
*BasicAuth `json:",inline,omitempty"`
Token *string `json:"token,omitempty"`
}
// GrafanaList list for Grafana
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type GrafanaList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Grafana `json:"items"`
}
var _ resource.Object = &Grafana{}
var _ rest.Getter = &Grafana{}
var _ rest.Lister = &Grafana{}
var _ rest.CreaterUpdater = &Grafana{}
var _ rest.Patcher = &Grafana{}
var _ rest.GracefulDeleter = &Grafana{}
// GetObjectMeta returns the object meta reference.
func (in *Grafana) GetObjectMeta() *metav1.ObjectMeta {
return &in.ObjectMeta
}
// NamespaceScoped returns if the object must be in a namespace.
func (in *Grafana) NamespaceScoped() bool {
return false
}
// New returns a new instance of the resource
func (in *Grafana) New() runtime.Object {
return &Grafana{}
}
// Destroy .
func (in *Grafana) Destroy() {}
// NewList return a new list instance of the resource
func (in *Grafana) NewList() runtime.Object {
return &GrafanaList{}
}
// GetGroupVersionResource returns the GroupVersionResource for this resource.
func (in *Grafana) GetGroupVersionResource() schema.GroupVersionResource {
return GroupVersion.WithResource(GrafanaResource)
}
// IsStorageVersion returns true if the object is also the internal version
func (in *Grafana) IsStorageVersion() bool {
return true
}
// ShortNames delivers a list of short names for a resource.
func (in *Grafana) ShortNames() []string {
return []string{"gf", "grafana-instance"}
}
const (
grafanaSecretNamePrefix = "grafana."
grafanaSecretEndpointAnnotationKey = "o11y.oam.dev/grafana-endpoint"
grafanaSecretUsernameKey = "username"
grafanaSecretPasswordKey = "password"
grafanaSecretTokenKey = "token"
)
// Get finds a resource in the storage by name and returns it.
func (in *Grafana) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return NewGrafanaClient(singleton.KubeClient.Get()).Get(ctx, name)
}
// List selects resources in the storage which match to the selector. 'options' can be nil.
func (in *Grafana) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
return NewGrafanaClient(singleton.KubeClient.Get()).List(ctx, apiserver.NewMatchingLabelSelectorFromInternalVersionListOptions(options))
}
// Create creates a new version of a resource.
func (in *Grafana) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
return obj, NewGrafanaClient(singleton.KubeClient.Get()).Create(ctx, obj.(*Grafana))
}
// Update finds a resource in the storage and updates it.
func (in *Grafana) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (obj runtime.Object, _ bool, err error) {
cli := NewGrafanaClient(singleton.KubeClient.Get())
if obj, err = cli.Get(ctx, name); err != nil {
return nil, false, err
}
if obj, err = objInfo.UpdatedObject(ctx, obj); err != nil {
return nil, false, err
}
return obj, false, cli.Update(ctx, obj.(*Grafana))
}
// Delete finds a resource in the storage and deletes it.
func (in *Grafana) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (obj runtime.Object, _ bool, err error) {
cli := NewGrafanaClient(singleton.KubeClient.Get())
if obj, err = cli.Get(ctx, name); err != nil {
return nil, false, err
}
return obj, true, cli.Delete(ctx, obj.(*Grafana))
}
// TODO add access check subresource

View File

@ -0,0 +1,164 @@
/*
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 v1alpha1
import (
"context"
"github.com/kubevela/pkg/util/k8s"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/utils/pointer"
"github.com/kubevela/pkg/util/singleton"
"github.com/kubevela/prism/pkg/apis/o11y/config"
)
var _ = Describe("Test Grafana API", func() {
BeforeEach(func() {
Ω(k8s.EnsureNamespace(context.Background(), singleton.KubeClient.Get(), config.ObservabilityNamespace)).To(Succeed())
})
AfterEach(func() {
Ω(k8s.ClearNamespace(context.Background(), singleton.KubeClient.Get(), config.ObservabilityNamespace)).To(Succeed())
})
It("Test Grafana API", func() {
s := &Grafana{}
By("Test meta info")
By("Test meta info")
Ω(s.New()).To(Equal(&Grafana{}))
Ω(s.NamespaceScoped()).To(BeFalse())
Ω(s.ShortNames()).To(ContainElement("gf"))
Ω(s.GetGroupVersionResource().GroupVersion()).To(Equal(GroupVersion))
Ω(s.GetGroupVersionResource().Resource).To(Equal(GrafanaResource))
Ω(s.IsStorageVersion()).To(BeTrue())
Ω(s.NewList()).To(Equal(&GrafanaList{}))
ctx := context.Background()
By("Test Create Grafana")
g1 := &Grafana{
ObjectMeta: metav1.ObjectMeta{Name: "example1", Labels: map[string]string{"key": "value"}},
Spec: GrafanaSpec{Endpoint: "1", Access: AccessCredential{Token: pointer.String("-")}},
}
g2 := &Grafana{
ObjectMeta: metav1.ObjectMeta{Name: "example2", Labels: map[string]string{"key": "value"}},
Spec: GrafanaSpec{Endpoint: "2", Access: AccessCredential{BasicAuth: &BasicAuth{Username: "-", Password: "-"}}},
}
g3 := &Grafana{
ObjectMeta: metav1.ObjectMeta{Name: "example3", Annotations: map[string]string{"key": "value"}},
Spec: GrafanaSpec{Endpoint: "3", Access: AccessCredential{BasicAuth: &BasicAuth{Username: "-", Password: "-"}}},
}
for _, g := range []*Grafana{g1, g2, g3} {
_, err := s.Create(ctx, g, nil, nil)
Ω(err).To(Succeed())
}
By("Create secret for distinguish test")
secret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "grafana.bad1"},
}
secret2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "grafana.bad2", Annotations: map[string]string{grafanaSecretEndpointAnnotationKey: "-"}},
}
secret3 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "bad3"},
}
for _, secret := range []*corev1.Secret{secret1, secret2, secret3} {
secret.SetNamespace(config.ObservabilityNamespace)
Ω(singleton.KubeClient.Get().Create(context.Background(), secret)).To(Succeed())
}
By("Test Get Grafana")
obj, err := s.Get(ctx, "example1", nil)
Ω(err).To(Succeed())
grafana, ok := obj.(*Grafana)
Ω(ok).To(BeTrue())
Ω(grafana.Spec).To(Equal(g1.Spec))
Ω(grafana.GetCredentialType()).To(Equal(GrafanaCredentialTypeBearerToken))
obj, err = s.Get(ctx, "example2", nil)
Ω(err).To(Succeed())
grafana, ok = obj.(*Grafana)
Ω(ok).To(BeTrue())
Ω(grafana.Spec).To(Equal(g2.Spec))
Ω(grafana.GetCredentialType()).To(Equal(GrafanaCredentialTypeBasicAuth))
_, err = s.Get(ctx, "example4", nil)
Ω(err).To(Satisfy(errors.IsNotFound))
_, err = s.Get(ctx, "bad1", nil)
Ω(err).To(Equal(NewEmptyEndpointGrafanaSecretError()))
_, err = s.Get(ctx, "bad2", nil)
Ω(err).To(Equal(NewEmptyCredentialGrafanaSecretError()))
By("Test List Grafana")
objs, err := s.List(ctx, nil)
Ω(err).To(Succeed())
grafanas, ok := objs.(*GrafanaList)
Ω(ok).To(BeTrue())
Ω(len(grafanas.Items)).To(Equal(3))
objs, err = s.List(ctx, &metainternalversion.ListOptions{LabelSelector: labels.SelectorFromSet(map[string]string{"key": "value"})})
Ω(err).To(Succeed())
grafanas, ok = objs.(*GrafanaList)
Ω(ok).To(BeTrue())
Ω(len(grafanas.Items)).To(Equal(2))
By("Test print table")
_, err = s.ConvertToTable(ctx, grafana, nil)
Ω(err).To(Succeed())
_, err = s.ConvertToTable(ctx, grafanas, nil)
Ω(err).To(Succeed())
By("Test Update Grafana")
obj, _, err = s.Update(ctx, "example3", rest.DefaultUpdatedObjectInfo(nil, func(ctx context.Context, newObj runtime.Object, oldObj runtime.Object) (transformedNewObj runtime.Object, err error) {
obj := oldObj.(*Grafana).DeepCopy()
obj.Spec.Endpoint = "test"
obj.SetLabels(map[string]string{"key": "value"})
return obj, nil
}), nil, nil, false, nil)
Ω(err).To(Succeed())
grafana, ok = obj.(*Grafana)
Ω(ok).To(BeTrue())
Ω(grafana.Spec.Endpoint).To(Equal("test"))
objs, err = s.List(ctx, &metainternalversion.ListOptions{LabelSelector: labels.SelectorFromSet(map[string]string{"key": "value"})})
Ω(err).To(Succeed())
grafanas, ok = objs.(*GrafanaList)
Ω(ok).To(BeTrue())
Ω(len(grafanas.Items)).To(Equal(3))
By("Test Delete Grafana")
obj, _, err = s.Delete(ctx, "example2", nil, nil)
Ω(err).To(Succeed())
grafana, ok = obj.(*Grafana)
Ω(ok).To(BeTrue())
Ω(grafana.Spec.Endpoint).To(Equal("2"))
objs, err = s.List(ctx, &metainternalversion.ListOptions{LabelSelector: labels.SelectorFromSet(map[string]string{"key": "value"})})
Ω(err).To(Succeed())
grafanas, ok = objs.(*GrafanaList)
Ω(ok).To(BeTrue())
Ω(len(grafanas.Items)).To(Equal(2))
})
})

View File

@ -0,0 +1,140 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AccessCredential) DeepCopyInto(out *AccessCredential) {
*out = *in
if in.BasicAuth != nil {
in, out := &in.BasicAuth, &out.BasicAuth
*out = new(BasicAuth)
**out = **in
}
if in.Token != nil {
in, out := &in.Token, &out.Token
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessCredential.
func (in *AccessCredential) DeepCopy() *AccessCredential {
if in == nil {
return nil
}
out := new(AccessCredential)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BasicAuth) DeepCopyInto(out *BasicAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuth.
func (in *BasicAuth) DeepCopy() *BasicAuth {
if in == nil {
return nil
}
out := new(BasicAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Grafana) DeepCopyInto(out *Grafana) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Grafana.
func (in *Grafana) DeepCopy() *Grafana {
if in == nil {
return nil
}
out := new(Grafana)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Grafana) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GrafanaList) DeepCopyInto(out *GrafanaList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Grafana, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaList.
func (in *GrafanaList) DeepCopy() *GrafanaList {
if in == nil {
return nil
}
out := new(GrafanaList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *GrafanaList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GrafanaSpec) DeepCopyInto(out *GrafanaSpec) {
*out = *in
in.Access.DeepCopyInto(&out.Access)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaSpec.
func (in *GrafanaSpec) DeepCopy() *GrafanaSpec {
if in == nil {
return nil
}
out := new(GrafanaSpec)
in.DeepCopyInto(out)
return out
}

View File

@ -0,0 +1,105 @@
/*
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 v1alpha1
import (
"context"
"fmt"
"net/http"
"net/url"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubevela/pkg/util/apiserver"
grafanav1alpha1 "github.com/kubevela/prism/pkg/apis/o11y/grafana/v1alpha1"
"github.com/kubevela/prism/pkg/util/subresource"
)
// GrafanaDashboardClient client for grafana datasource
// +kubebuilder:object:generate=false
type GrafanaDashboardClient interface {
Get(ctx context.Context, name string) (*GrafanaDashboard, error)
List(ctx context.Context, options ...client.ListOption) (*GrafanaDashboardList, error)
Create(ctx context.Context, GrafanaDashboard *GrafanaDashboard) error
Update(ctx context.Context, GrafanaDashboard *GrafanaDashboard) error
Delete(ctx context.Context, GrafanaDashboard *GrafanaDashboard) error
}
// NewGrafanaDashboardClient create GrafanaDashboardClient
func NewGrafanaDashboardClient(cli client.Client) GrafanaDashboardClient {
return &grafanaDashboardClient{grafanav1alpha1.NewGrafanaClient(cli)}
}
type grafanaDashboardClient struct {
grafanav1alpha1.GrafanaClient
}
func (in *grafanaDashboardClient) Get(ctx context.Context, name string) (*GrafanaDashboard, error) {
resourceName := subresource.NewCompoundName(name)
dashboard := &GrafanaDashboard{
ObjectMeta: metav1.ObjectMeta{Name: resourceName.String(), UID: "-"},
}
return dashboard, grafanav1alpha1.NewGrafanaSubResourceRequest(dashboard, name).
WithMethod(http.MethodGet).
WithPathFunc(func() (string, error) {
return "/api/dashboards/uid/" + url.PathEscape(resourceName.SubResourceName), nil
}).
WithOnSuccess(dashboard.FromResponseBody).
Do(ctx, in.GrafanaClient)
}
func (in *grafanaDashboardClient) Create(ctx context.Context, dashboard *GrafanaDashboard) error {
return grafanav1alpha1.NewGrafanaSubResourceRequest(dashboard, dashboard.GetName()).
WithMethod(http.MethodPost).
WithPathFunc(func() (string, error) {
return "/api/dashboards/db", nil
}).
WithBodyFunc(dashboard.ToRequestBody).
Do(ctx, in.GrafanaClient)
}
func (in *grafanaDashboardClient) Update(ctx context.Context, dashboard *GrafanaDashboard) error {
return in.Create(ctx, dashboard)
}
func (in *grafanaDashboardClient) Delete(ctx context.Context, dashboard *GrafanaDashboard) error {
resourceName := subresource.NewCompoundName(dashboard.GetName())
return grafanav1alpha1.NewGrafanaSubResourceRequest(dashboard, dashboard.GetName()).
WithMethod(http.MethodDelete).
WithPathFunc(func() (string, error) {
return "/api/dashboards/uid/" + url.PathEscape(resourceName.SubResourceName), nil
}).
Do(ctx, in.GrafanaClient)
}
func (in *grafanaDashboardClient) List(ctx context.Context, options ...client.ListOption) (*GrafanaDashboardList, error) {
opts := apiserver.NewListOptions(options...)
parentResourceName := subresource.GetParentResourceNameFromLabelSelector(opts.LabelSelector, "grafana")
params := apiserver.BuildQueryParamsFromLabelSelector(opts.LabelSelector, "query", "tag", "folderIds", "dashboardIds", "starred")
dashboards := &GrafanaDashboardList{}
return dashboards, grafanav1alpha1.NewGrafanaSubResourceRequest(&grafanav1alpha1.Grafana{}, (&subresource.CompoundName{ParentResourceName: parentResourceName}).String()).
WithMethod(http.MethodGet).
WithPathFunc(func() (string, error) {
return fmt.Sprintf("/api/search?type=dash-db%s", params), nil
}).
WithOnSuccess(func(respBody []byte) error {
return dashboards.FromResponseBody(respBody, parentResourceName)
}).
Do(ctx, in.GrafanaClient)
}

View File

@ -0,0 +1,120 @@
/*
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 v1alpha1
import (
"encoding/json"
"fmt"
"strconv"
"k8s.io/apimachinery/pkg/runtime"
"github.com/kubevela/prism/pkg/util/subresource"
)
const (
grafanaDashboardFolderIdLabelKey = "o11y.oam.dev/grafana-dashboard-folder-id"
grafanaDashboardFolderUidLabelKey = "o11y.oam.dev/grafana-dashboard-folder-uid"
)
// ToRequestBody convert object into body for request
func (in *GrafanaDashboard) ToRequestBody() ([]byte, error) {
dashboard := map[string]interface{}{}
if err := json.Unmarshal(in.Spec.Raw, &dashboard); err != nil {
return nil, err
}
dashboard["uid"] = subresource.NewCompoundName(in.GetName()).SubResourceName
delete(dashboard, "id")
data := map[string]interface{}{"dashboard": dashboard}
if labels := in.GetLabels(); labels != nil {
if raw := labels[grafanaDashboardFolderIdLabelKey]; raw != "" {
id, err := strconv.Atoi(raw)
if err != nil {
return nil, err
}
data["folderId"] = id
}
if raw := labels[grafanaDashboardFolderUidLabelKey]; raw != "" {
data["folderUid"] = raw
}
}
return json.Marshal(data)
}
// FromResponseBody convert response into object
func (in *GrafanaDashboard) FromResponseBody(body []byte) error {
data := map[string]interface{}{}
if err := json.Unmarshal(body, &data); err != nil {
return err
}
dashboard, ok := data["dashboard"].(map[string]interface{})
if !ok {
return fmt.Errorf("no dashboard found in response body")
}
meta, _ := data["meta"].(map[string]interface{})
return in.load(dashboard, meta)
}
// FromResponseBody convert response into objects
func (in *GrafanaDashboardList) FromResponseBody(body []byte, parentResourceName string) error {
var data []map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return err
}
in.Items = make([]GrafanaDashboard, len(data))
for idx, raw := range data {
gdb := &GrafanaDashboard{}
uid, ok := raw["uid"].(string)
if !ok {
return fmt.Errorf("invalid dashboard response, no valid uid found")
}
gdb.SetName((&subresource.CompoundName{ParentResourceName: parentResourceName, SubResourceName: uid}).String())
dashboard := map[string]interface{}{}
for _, key := range []string{"title", "id", "tags"} {
if raw[key] != nil {
dashboard[key] = raw[key]
}
}
if err := gdb.load(dashboard, raw); err != nil {
return err
}
in.Items[idx] = *gdb
}
return nil
}
func (in *GrafanaDashboard) load(dashboard map[string]interface{}, meta map[string]interface{}) error {
if meta != nil {
labels := in.GetLabels()
if labels == nil {
labels = map[string]string{}
}
if id, validId := meta["folderId"].(float64); validId {
labels[grafanaDashboardFolderIdLabelKey] = strconv.Itoa(int(id))
}
if uid, validUid := meta["folderUid"].(string); validUid {
labels[grafanaDashboardFolderUidLabelKey] = uid
}
in.SetLabels(labels)
}
bs, err := json.Marshal(dashboard)
if err != nil {
return err
}
in.Spec = runtime.RawExtension{Raw: bs}
return nil
}

View File

@ -0,0 +1,71 @@
/*
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 v1alpha1
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
)
func TestGrafanaDashboardToRequestBody(t *testing.T) {
in := &GrafanaDashboard{}
in.SetName("test@local")
in.Spec = runtime.RawExtension{Raw: []byte(`{"key":"val"}`)}
in.SetLabels(map[string]string{
grafanaDashboardFolderIdLabelKey: "1",
grafanaDashboardFolderUidLabelKey: "uid",
})
bs, err := in.ToRequestBody()
require.NoError(t, err)
var m1, m2 map[string]interface{}
require.NoError(t, json.Unmarshal(bs, &m1))
require.NoError(t, json.Unmarshal([]byte(`{"dashboard":{"uid":"test","key":"val"},"folderId":1,"folderUid":"uid"}`), &m2))
require.Equal(t, m2, m1)
// test bad label
in.SetLabels(map[string]string{grafanaDashboardFolderIdLabelKey: "bad"})
_, err = in.ToRequestBody()
require.NotNil(t, err)
// test bad spec
in.Spec = runtime.RawExtension{Raw: []byte(`bad`)}
in.SetLabels(nil)
_, err = in.ToRequestBody()
require.NotNil(t, err)
}
func TestGrafanaDashboardFromResponseBody(t *testing.T) {
in := &GrafanaDashboard{}
require.NotNil(t, in.FromResponseBody([]byte(`bad`)))
require.Errorf(t, in.FromResponseBody([]byte(`{}`)), "no dashboard found in response body")
// test full load
require.NoError(t, in.FromResponseBody([]byte(`{"dashboard":{"uid":"test","key":"val"},"meta":{"folderId":1,"folderUid":"a"}}`)))
require.Equal(t, []byte(`{"key":"val","uid":"test"}`), in.Spec.Raw)
require.Equal(t, "1", in.GetLabels()[grafanaDashboardFolderIdLabelKey])
require.Equal(t, "a", in.GetLabels()[grafanaDashboardFolderUidLabelKey])
}
func TestGrafanaDashboardListFromResponseBody(t *testing.T) {
in := &GrafanaDashboardList{}
require.NotNil(t, in.FromResponseBody([]byte(`[bad]`), "test"))
require.Errorf(t, in.FromResponseBody([]byte(`[{}]`), "test"), "invalid dashboard response, no valid uid found")
require.NoError(t, in.FromResponseBody([]byte(`[{"uid":"a","title":"A"},{"uid":"b","title":"B"}]`), "test"))
require.Equal(t, len(in.Items), 2)
require.Equal(t, []byte(`{"title":"A"}`), in.Items[0].Spec.Raw)
require.Equal(t, "a@test", in.Items[0].Name)
}

View File

@ -0,0 +1,26 @@
/*
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.
*/
// Api versions allow the api contract for a resource to be changed while keeping
// backward compatibility by support multiple concurrent versions
// of the same resource
// Package v1alpha1 contains types required for v1alpha1
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package,register
// +k8s:defaulter-gen=TypeMeta
// +groupName=o11y.prism.oam.dev
package v1alpha1

View File

@ -0,0 +1,82 @@
/*
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 v1alpha1
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/kubevela/pkg/util/apiserver"
"github.com/kubevela/prism/pkg/util/subresource"
)
// ConvertToTable convert resource to table
func (in *GrafanaDashboard) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
switch obj := object.(type) {
case *GrafanaDashboard:
return printGrafanaDashboard(obj), nil
case *GrafanaDashboardList:
return printGrafanaDashboardList(obj), nil
default:
return nil, fmt.Errorf("unknown type %T", object)
}
}
var (
definitions = []metav1.TableColumnDefinition{
{Name: "UID", Type: "string", Format: "name", Description: "the name of the GrafanaDashboard"},
{Name: "Title", Type: "string", Description: "the title of the GrafanaDashboard"},
{Name: "FolderId", Type: "string", Description: "the folder id of the grafana dashboard", Priority: 10},
}
)
func printGrafanaDashboard(in *GrafanaDashboard) *metav1.Table {
return &metav1.Table{
ColumnDefinitions: definitions,
Rows: []metav1.TableRow{printGrafanaDashboardRow(in)},
}
}
func printGrafanaDashboardList(in *GrafanaDashboardList) *metav1.Table {
t := &metav1.Table{
ColumnDefinitions: definitions,
}
for _, c := range in.Items {
t.Rows = append(t.Rows, printGrafanaDashboardRow(c.DeepCopy()))
}
return t
}
func printGrafanaDashboardRow(c *GrafanaDashboard) metav1.TableRow {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: c},
}
var folderId string
if labels := c.GetLabels(); labels != nil {
folderId = labels[grafanaDashboardFolderIdLabelKey]
}
row.Cells = append(row.Cells,
subresource.NewCompoundName(c.Name).SubResourceName,
apiserver.GetStringFromRawExtension(&c.Spec, "title"),
folderId,
)
return row
}

View File

@ -0,0 +1,63 @@
/*
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
)
const (
// Group the group for the apiextensions
Group = "o11y.prism.oam.dev"
// Version the version for the v1alpha1 apiextensions
Version = "v1alpha1"
)
func init() {
if err := AddToScheme(scheme.Scheme); err != nil {
klog.Fatalf("failed registering api types")
}
}
// AddToScheme add virtual cluster scheme
var AddToScheme = func(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, GroupVersion)
// +kubebuilder:scaffold:install
scheme.AddKnownTypes(GroupVersion,
&GrafanaDashboard{},
&GrafanaDashboardList{},
)
return nil
}
// GroupVersion the apiextensions v1alpha1 group version
var GroupVersion = schema.GroupVersion{Group: Group, Version: Version}
var (
// GrafanaDashboardResource resource name for GrafanaDashboard
GrafanaDashboardResource = "grafanadashboards"
// GrafanaDashboardKind kind name for GrafanaDashboard
GrafanaDashboardKind = "GrafanaDashboard"
// GrafanaDashboardGroupResource GroupResource for GrafanaDashboard
GrafanaDashboardGroupResource = schema.GroupResource{Group: Group, Resource: GrafanaDashboardResource}
// GrafanaDashboardGroupVersionKind GroupVersionKind for GrafanaDashboard
GrafanaDashboardGroupVersionKind = GroupVersion.WithKind(GrafanaDashboardKind)
)

View File

@ -0,0 +1,179 @@
/*
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 v1alpha1
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/kubevela/pkg/util/k8s"
"github.com/kubevela/pkg/util/singleton"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/utils/pointer"
"github.com/kubevela/prism/pkg/apis/o11y/config"
grafanav1alpha1 "github.com/kubevela/prism/pkg/apis/o11y/grafana/v1alpha1"
"github.com/kubevela/prism/pkg/util/subresource"
_ "github.com/kubevela/prism/test/bootstrap"
)
func TestGrafanaDashboard(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "GrafanaDashboard Extension API Test")
}
var _ = Describe("Test GrafanaDashboard API", func() {
var mockServer *httptest.Server
var data map[string][]byte
BeforeEach(func() {
Ω(k8s.EnsureNamespace(context.Background(), singleton.KubeClient.Get(), config.ObservabilityNamespace)).To(Succeed())
data = map[string][]byte{}
mockServer = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
p := request.Method + " " + request.URL.Path
switch {
case p == "POST /api/dashboards/db":
bs, _ := io.ReadAll(request.Body)
var m map[string]interface{}
_ = json.Unmarshal(bs, &m)
dashboard := m["dashboard"].(map[string]interface{})
uid := dashboard["uid"].(string)
bs, _ = json.Marshal(dashboard)
data[uid] = bs
writer.WriteHeader(http.StatusOK)
case strings.HasPrefix(p, "GET /api/dashboards/uid/"):
uid := strings.TrimPrefix(p, "GET /api/dashboards/uid/")
db, ok := data[uid]
if ok {
_, _ = writer.Write([]byte(fmt.Sprintf(`{"dashboard":%s}`, db)))
writer.WriteHeader(http.StatusOK)
} else {
writer.WriteHeader(http.StatusNotFound)
}
case strings.HasPrefix(p, "GET /api/search"):
var dbs []string
for _, val := range data {
dbs = append(dbs, string(val))
}
_, _ = writer.Write([]byte("[" + strings.Join(dbs, ",") + "]"))
writer.WriteHeader(http.StatusOK)
case strings.HasPrefix(p, "DELETE /api/dashboards/uid/"):
uid := strings.TrimPrefix(p, "DELETE /api/dashboards/uid/")
if _, ok := data[uid]; ok {
delete(data, uid)
writer.WriteHeader(http.StatusOK)
} else {
writer.WriteHeader(http.StatusNotFound)
}
default:
writer.WriteHeader(http.StatusNotFound)
}
}))
})
AfterEach(func() {
Ω(k8s.ClearNamespace(context.Background(), singleton.KubeClient.Get(), config.ObservabilityNamespace)).To(Succeed())
mockServer.Close()
})
It("Test GrafanaDashboard API", func() {
s := &GrafanaDashboard{}
By("Test meta info")
By("Test meta info")
Ω(s.New()).To(Equal(&GrafanaDashboard{}))
Ω(s.GetObjectMeta()).To(Equal(&metav1.ObjectMeta{}))
Ω(s.NamespaceScoped()).To(BeFalse())
Ω(s.ShortNames()).To(ContainElement("gdb"))
Ω(s.GetGroupVersionResource().GroupVersion()).To(Equal(GroupVersion))
Ω(s.GetGroupVersionResource().Resource).To(Equal(GrafanaDashboardResource))
Ω(s.IsStorageVersion()).To(BeTrue())
Ω(s.NewList()).To(Equal(&GrafanaDashboardList{}))
ctx := context.Background()
By("Create Grafana")
grafana := &grafanav1alpha1.Grafana{
ObjectMeta: metav1.ObjectMeta{Name: subresource.DefaultParentResourceName},
Spec: grafanav1alpha1.GrafanaSpec{
Endpoint: mockServer.URL,
Access: grafanav1alpha1.AccessCredential{Token: pointer.String("mock")},
},
}
_, err := (&grafanav1alpha1.Grafana{}).Create(ctx, grafana, nil, nil)
Ω(err).To(Succeed())
By("Test Create GrafanaDashboard")
_, err = s.Create(ctx, &GrafanaDashboard{
ObjectMeta: metav1.ObjectMeta{Name: "alpha"},
Spec: runtime.RawExtension{Raw: []byte(`{"key":"val"}`)},
}, nil, nil)
Ω(err).To(Succeed())
_, err = s.Create(ctx, &GrafanaDashboard{
ObjectMeta: metav1.ObjectMeta{Name: "beta"},
Spec: runtime.RawExtension{Raw: []byte(`{"key":"value"}`)},
}, nil, nil)
Ω(err).To(Succeed())
By("Test Update GrafanaDashboard")
_, _, err = s.Update(ctx, "beta", rest.DefaultUpdatedObjectInfo(&GrafanaDashboard{
ObjectMeta: metav1.ObjectMeta{Name: "beta"},
Spec: runtime.RawExtension{Raw: []byte(`{"key":"v"}`)},
}), nil, nil, false, nil)
Ω(err).To(Succeed())
By("Test Get GrafanaDashboard")
obj, err := s.Get(ctx, "alpha", nil)
Ω(err).To(Succeed())
gdb, ok := obj.(*GrafanaDashboard)
Ω(ok).To(BeTrue())
Ω(gdb.Spec.Raw).To(Equal([]byte(`{"key":"val","uid":"alpha"}`)))
By("Test List GrafanaDashboard")
objs, err := s.List(ctx, nil)
Ω(err).To(Succeed())
dbs, ok := objs.(*GrafanaDashboardList)
Ω(ok).To(BeTrue())
Ω(len(dbs.Items)).To(Equal(2))
By("Test Delete GrafanaDashboard")
_, _, err = s.Delete(ctx, "alpha", nil, nil)
Ω(err).To(Succeed())
objs, err = s.List(ctx, nil)
Ω(err).To(Succeed())
dbs, ok = objs.(*GrafanaDashboardList)
Ω(ok).To(BeTrue())
Ω(len(dbs.Items)).To(Equal(1))
By("Test GrafanaDashboard Printer")
_, err = s.ConvertToTable(ctx, obj, nil)
Ω(err).To(Succeed())
_, err = s.ConvertToTable(ctx, objs, nil)
Ω(err).To(Succeed())
})
})

View File

@ -0,0 +1,129 @@
/*
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 v1alpha1
import (
"context"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
"github.com/kubevela/pkg/util/apiserver"
"github.com/kubevela/pkg/util/singleton"
)
// GrafanaDashboard is a reflection api for Grafana Datasource
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type GrafanaDashboard struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// +kubebuilder:pruning:PreserveUnknownFields
Spec runtime.RawExtension `json:"spec,omitempty"`
}
// GrafanaDashboardList list for GrafanaDashboard
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type GrafanaDashboardList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []GrafanaDashboard `json:"items"`
}
var _ resource.Object = &GrafanaDashboard{}
var _ rest.Getter = &GrafanaDashboard{}
var _ rest.CreaterUpdater = &GrafanaDashboard{}
var _ rest.Patcher = &GrafanaDashboard{}
var _ rest.GracefulDeleter = &GrafanaDashboard{}
var _ rest.Lister = &GrafanaDashboard{}
// GetObjectMeta returns the object meta reference.
func (in *GrafanaDashboard) GetObjectMeta() *metav1.ObjectMeta {
return &in.ObjectMeta
}
// NamespaceScoped returns if the object must be in a namespace.
func (in *GrafanaDashboard) NamespaceScoped() bool {
return false
}
// New returns a new instance of the resource
func (in *GrafanaDashboard) New() runtime.Object {
return &GrafanaDashboard{}
}
// Destroy .
func (in *GrafanaDashboard) Destroy() {}
// NewList return a new list instance of the resource
func (in *GrafanaDashboard) NewList() runtime.Object {
return &GrafanaDashboardList{}
}
// GetGroupVersionResource returns the GroupVersionResource for this resource.
func (in *GrafanaDashboard) GetGroupVersionResource() schema.GroupVersionResource {
return GroupVersion.WithResource(GrafanaDashboardResource)
}
// IsStorageVersion returns true if the object is also the internal version
func (in *GrafanaDashboard) IsStorageVersion() bool {
return true
}
// ShortNames delivers a list of short names for a resource.
func (in *GrafanaDashboard) ShortNames() []string {
return []string{"gdb", "datasource", "datasources", "grafana-datasource", "grafana-datasources"}
}
// Get finds a resource in the storage by name and returns it.
func (in *GrafanaDashboard) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return NewGrafanaDashboardClient(singleton.KubeClient.Get()).Get(ctx, name)
}
func (in *GrafanaDashboard) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
return obj, NewGrafanaDashboardClient(singleton.KubeClient.Get()).Create(ctx, obj.(*GrafanaDashboard))
}
func (in *GrafanaDashboard) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (obj runtime.Object, _ bool, err error) {
cli := NewGrafanaDashboardClient(singleton.KubeClient.Get())
if obj, err = cli.Get(ctx, name); err != nil {
return nil, false, err
}
if obj, err = objInfo.UpdatedObject(ctx, obj); err != nil {
return nil, false, err
}
return obj, false, cli.Update(ctx, obj.(*GrafanaDashboard))
}
func (in *GrafanaDashboard) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (obj runtime.Object, _ bool, err error) {
cli := NewGrafanaDashboardClient(singleton.KubeClient.Get())
if obj, err = cli.Get(ctx, name); err != nil {
return nil, false, err
}
return obj, true, cli.Delete(ctx, obj.(*GrafanaDashboard))
}
func (in *GrafanaDashboard) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
if name := apiserver.GetMetadataNameInFieldSelectorFromInternalVersionListOptions(options); name != nil {
return NewGrafanaDashboardClient(singleton.KubeClient.Get()).Get(ctx, *name)
}
return NewGrafanaDashboardClient(singleton.KubeClient.Get()).List(ctx, apiserver.NewMatchingLabelSelectorFromInternalVersionListOptions(options))
}

View File

@ -0,0 +1,84 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GrafanaDashboard) DeepCopyInto(out *GrafanaDashboard) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboard.
func (in *GrafanaDashboard) DeepCopy() *GrafanaDashboard {
if in == nil {
return nil
}
out := new(GrafanaDashboard)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *GrafanaDashboard) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GrafanaDashboardList) DeepCopyInto(out *GrafanaDashboardList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]GrafanaDashboard, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboardList.
func (in *GrafanaDashboardList) DeepCopy() *GrafanaDashboardList {
if in == nil {
return nil
}
out := new(GrafanaDashboardList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *GrafanaDashboardList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View File

@ -0,0 +1,116 @@
/*
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 v1alpha1
import (
"context"
"fmt"
"net/http"
"net/url"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubevela/pkg/util/apiserver"
grafanav1alpha1 "github.com/kubevela/prism/pkg/apis/o11y/grafana/v1alpha1"
"github.com/kubevela/prism/pkg/util/subresource"
)
// GrafanaDatasourceClient client for grafana datasource
// +kubebuilder:object:generate=false
type GrafanaDatasourceClient interface {
Get(ctx context.Context, name string) (*GrafanaDatasource, error)
List(ctx context.Context, options ...client.ListOption) (*GrafanaDatasourceList, error)
Create(ctx context.Context, grafanaDatasource *GrafanaDatasource) error
Update(ctx context.Context, grafanaDatasource *GrafanaDatasource) error
Delete(ctx context.Context, grafanaDatasource *GrafanaDatasource) error
}
// NewGrafanaDatasourceClient create GrafanaDatasourceClient
func NewGrafanaDatasourceClient(cli client.Client) GrafanaDatasourceClient {
return &grafanaDatasourceClient{grafanav1alpha1.NewGrafanaClient(cli)}
}
type grafanaDatasourceClient struct {
grafanav1alpha1.GrafanaClient
}
func (in *grafanaDatasourceClient) Get(ctx context.Context, name string) (*GrafanaDatasource, error) {
resourceName := subresource.NewCompoundName(name)
datasource := &GrafanaDatasource{
ObjectMeta: metav1.ObjectMeta{Name: resourceName.String(), UID: "-"},
}
return datasource, grafanav1alpha1.NewGrafanaSubResourceRequest(datasource, name).
WithMethod(http.MethodGet).
WithPathFunc(func() (string, error) {
return "/api/datasources/uid/" + url.PathEscape(resourceName.SubResourceName), nil
}).
WithOnSuccess(func(respBody []byte) error {
datasource.Spec = runtime.RawExtension{Raw: respBody}
return nil
}).
Do(ctx, in.GrafanaClient)
}
func (in *grafanaDatasourceClient) Create(ctx context.Context, datasource *GrafanaDatasource) error {
return grafanav1alpha1.NewGrafanaSubResourceRequest(datasource, datasource.GetName()).
WithMethod(http.MethodPost).
WithPathFunc(func() (string, error) {
return "/api/datasources/", nil
}).
WithBodyFunc(datasource.ToRequestBody).
WithOnSuccess(datasource.FromResponseBody).
Do(ctx, in.GrafanaClient)
}
func (in *grafanaDatasourceClient) Update(ctx context.Context, datasource *GrafanaDatasource) error {
return grafanav1alpha1.NewGrafanaSubResourceRequest(datasource, datasource.GetName()).
WithMethod(http.MethodPut).
WithPathFunc(func() (string, error) {
id, err := datasource.GetID()
return fmt.Sprintf("/api/datasources/%d", id), err
}).
WithBodyFunc(datasource.ToRequestBody).
WithOnSuccess(datasource.FromResponseBody).
Do(ctx, in.GrafanaClient)
}
func (in *grafanaDatasourceClient) Delete(ctx context.Context, datasource *GrafanaDatasource) error {
return grafanav1alpha1.NewGrafanaSubResourceRequest(datasource, datasource.GetName()).
WithMethod(http.MethodDelete).
WithPathFunc(func() (string, error) {
return "/api/datasources/uid/" + subresource.NewCompoundName(datasource.GetName()).SubResourceName, nil
}).
Do(ctx, in.GrafanaClient)
}
func (in *grafanaDatasourceClient) List(ctx context.Context, options ...client.ListOption) (*GrafanaDatasourceList, error) {
opts := apiserver.NewListOptions(options...)
parentResourceName := subresource.GetParentResourceNameFromLabelSelector(opts.LabelSelector, "grafana")
datasources := &GrafanaDatasourceList{}
return datasources, grafanav1alpha1.NewGrafanaSubResourceRequest(&grafanav1alpha1.Grafana{}, (&subresource.CompoundName{ParentResourceName: parentResourceName}).String()).
WithMethod(http.MethodGet).
WithPathFunc(func() (string, error) {
return "/api/datasources", nil
}).
WithOnSuccess(func(respBody []byte) error {
return datasources.FromResponseBody(respBody, parentResourceName)
}).
Do(ctx, in.GrafanaClient)
}

View File

@ -0,0 +1,84 @@
/*
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 v1alpha1
import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"github.com/kubevela/prism/pkg/util/subresource"
)
// GetID get id from GrafanaDatasource
func (in *GrafanaDatasource) GetID() (int, error) {
obj := struct {
ID int `json:"id"`
}{}
return obj.ID, json.Unmarshal(in.Spec.Raw, &obj)
}
// ToRequestBody convert object into body for request
func (in *GrafanaDatasource) ToRequestBody() ([]byte, error) {
datasource := map[string]interface{}{}
if err := json.Unmarshal(in.Spec.Raw, &datasource); err != nil {
return nil, err
}
datasource["uid"] = subresource.NewCompoundName(in.GetName()).SubResourceName
return json.Marshal(datasource)
}
// FromResponseBody load datasource from grafana api create/update response
func (in *GrafanaDatasource) FromResponseBody(respBody []byte) error {
obj := &struct {
DataSource map[string]interface{} `json:"datasource"`
}{}
if err := json.Unmarshal(respBody, obj); err != nil {
return err
}
bs, err := json.Marshal(obj.DataSource)
if err != nil {
return err
}
in.Spec = runtime.RawExtension{Raw: bs}
return err
}
// FromResponseBody load datasources from grafana api
func (in *GrafanaDatasourceList) FromResponseBody(respBody []byte, parentResourceName string) error {
data := []map[string]interface{}{}
if err := json.Unmarshal(respBody, &data); err != nil {
return err
}
in.Items = []GrafanaDatasource{}
for _, raw := range data {
ds := &GrafanaDatasource{}
uid, ok := raw["uid"].(string)
if !ok {
return fmt.Errorf("invalid grafana datasource response, no valid uid found")
}
ds.SetName((&subresource.CompoundName{ParentResourceName: parentResourceName, SubResourceName: uid}).String())
bs, err := json.Marshal(raw)
if err != nil {
return err
}
ds.Spec = runtime.RawExtension{Raw: bs}
in.Items = append(in.Items, *ds)
}
return nil
}

View File

@ -0,0 +1,53 @@
/*
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 v1alpha1
import (
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func TestGrafanaDatasourceToRequestBody(t *testing.T) {
in := &GrafanaDatasource{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
in.Spec = runtime.RawExtension{Raw: []byte(`bad`)}
_, err := in.ToRequestBody()
require.NotNil(t, err)
in.Spec = runtime.RawExtension{Raw: []byte(`{"key":"val"}`)}
bs, err := in.ToRequestBody()
require.NoError(t, err)
require.Equal(t, []byte(`{"key":"val","uid":"test"}`), bs)
}
func TestGrafanaDatasourceFromResponseBody(t *testing.T) {
in := &GrafanaDatasource{}
require.NotNil(t, in.FromResponseBody([]byte(`bad`)))
require.NoError(t, in.FromResponseBody([]byte(`{"datasource":{"key":"val"}}`)))
require.Equal(t, []byte(`{"key":"val"}`), in.Spec.Raw)
}
func TestGrafanaDatasourceListFromResponseBody(t *testing.T) {
in := &GrafanaDatasourceList{}
require.NotNil(t, in.FromResponseBody([]byte(`bad`), "test"))
require.Errorf(t, in.FromResponseBody([]byte(`[{}]`), "test"), "invalid grafana datasource response, no valid uid found")
require.NoError(t, in.FromResponseBody([]byte(`[{"uid":"a","key":"A"},{"uid":"b","key":"B"}]`), "test"))
require.Equal(t, 2, len(in.Items))
require.Equal(t, "a@test", in.Items[0].GetName())
require.Equal(t, []byte(`{"key":"A","uid":"a"}`), in.Items[0].Spec.Raw)
}

View File

@ -0,0 +1,26 @@
/*
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.
*/
// Api versions allow the api contract for a resource to be changed while keeping
// backward compatibility by support multiple concurrent versions
// of the same resource
// Package v1alpha1 contains types required for v1alpha1
// +k8s:openapi-gen=true
// +k8s:deepcopy-gen=package,register
// +k8s:defaulter-gen=TypeMeta
// +groupName=o11y.prism.oam.dev
package v1alpha1

View File

@ -0,0 +1,80 @@
/*
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 v1alpha1
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/kubevela/pkg/util/apiserver"
"github.com/kubevela/prism/pkg/util/subresource"
)
// ConvertToTable convert resource to table
func (in *GrafanaDatasource) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
switch obj := object.(type) {
case *GrafanaDatasource:
return printGrafanaDatasource(obj), nil
case *GrafanaDatasourceList:
return printGrafanaDatasourceList(obj), nil
default:
return nil, fmt.Errorf("unknown type %T", object)
}
}
var (
definitions = []metav1.TableColumnDefinition{
{Name: "UID", Type: "string", Format: "name", Description: "the uid of the GrafanaDatasource"},
{Name: "Name", Type: "string", Format: "name", Description: "the name of the GrafanaDatasource"},
{Name: "Type", Type: "string", Description: "the type of the GrafanaDatasource"},
{Name: "URL", Type: "string", Description: "the url of the GrafanaDatasource"},
}
)
func printGrafanaDatasource(in *GrafanaDatasource) *metav1.Table {
return &metav1.Table{
ColumnDefinitions: definitions,
Rows: []metav1.TableRow{printGrafanaDatasourceRow(in)},
}
}
func printGrafanaDatasourceList(in *GrafanaDatasourceList) *metav1.Table {
t := &metav1.Table{
ColumnDefinitions: definitions,
}
for _, c := range in.Items {
t.Rows = append(t.Rows, printGrafanaDatasourceRow(c.DeepCopy()))
}
return t
}
func printGrafanaDatasourceRow(c *GrafanaDatasource) metav1.TableRow {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: c},
}
row.Cells = append(row.Cells,
subresource.NewCompoundName(c.Name).SubResourceName,
apiserver.GetStringFromRawExtension(&c.Spec, "name"),
apiserver.GetStringFromRawExtension(&c.Spec, "type"),
apiserver.GetStringFromRawExtension(&c.Spec, "url"),
)
return row
}

View File

@ -0,0 +1,63 @@
/*
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 v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
)
const (
// Group the group for the apiextensions
Group = "o11y.prism.oam.dev"
// Version the version for the v1alpha1 apiextensions
Version = "v1alpha1"
)
func init() {
if err := AddToScheme(scheme.Scheme); err != nil {
klog.Fatalf("failed registering api types")
}
}
// AddToScheme add virtual cluster scheme
var AddToScheme = func(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, GroupVersion)
// +kubebuilder:scaffold:install
scheme.AddKnownTypes(GroupVersion,
&GrafanaDatasource{},
&GrafanaDatasourceList{},
)
return nil
}
// GroupVersion the apiextensions v1alpha1 group version
var GroupVersion = schema.GroupVersion{Group: Group, Version: Version}
var (
// GrafanaDatasourceResource resource name for GrafanaDatasource
GrafanaDatasourceResource = "grafanadatasources"
// GrafanaDatasourceKind kind name for GrafanaDatasource
GrafanaDatasourceKind = "GrafanaDatasource"
// GrafanaDatasourceGroupResource GroupResource for GrafanaDatasource
GrafanaDatasourceGroupResource = schema.GroupResource{Group: Group, Resource: GrafanaDatasourceResource}
// GrafanaDatasourceGroupVersionKind GroupVersionKind for GrafanaDatasource
GrafanaDatasourceGroupVersionKind = GroupVersion.WithKind(GrafanaDatasourceKind)
)

View File

@ -0,0 +1,191 @@
/*
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 v1alpha1
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/kubevela/pkg/util/k8s"
"github.com/kubevela/pkg/util/singleton"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/utils/pointer"
"github.com/kubevela/pkg/util/apiserver"
"github.com/kubevela/prism/pkg/apis/o11y/config"
grafanav1alpha1 "github.com/kubevela/prism/pkg/apis/o11y/grafana/v1alpha1"
"github.com/kubevela/prism/pkg/util/subresource"
_ "github.com/kubevela/prism/test/bootstrap"
)
func TestGrafanaDatasource(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "GrafanaDatasource Extension API Test")
}
var _ = Describe("Test GrafanaDatasource API", func() {
var mockServer *httptest.Server
var data map[string][]byte
BeforeEach(func() {
Ω(k8s.EnsureNamespace(context.Background(), singleton.KubeClient.Get(), config.ObservabilityNamespace)).To(Succeed())
data = map[string][]byte{}
mockServer = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
p := request.Method + " " + request.URL.Path
switch {
case p == "POST /api/datasources/":
bs, _ := io.ReadAll(request.Body)
uid := apiserver.GetStringFromRawExtension(&runtime.RawExtension{Raw: bs}, "uid")
data[uid] = bs
_, _ = writer.Write(bs)
writer.WriteHeader(http.StatusOK)
case strings.HasPrefix(p, "PUT /api/datasources/"):
id := strings.TrimPrefix(p, "PUT /api/datasources/")
bs, _ := io.ReadAll(request.Body)
for k, v := range data {
var m map[string]interface{}
_ = json.Unmarshal(v, &m)
if fmt.Sprintf("%d", int(m["id"].(float64))) == id {
data[k] = bs
break
}
}
_, _ = writer.Write(bs)
writer.WriteHeader(http.StatusOK)
case strings.HasPrefix(p, "GET /api/datasources/uid/"):
uid := strings.TrimPrefix(p, "GET /api/datasources/uid/")
db, ok := data[uid]
if ok {
_, _ = writer.Write(db)
writer.WriteHeader(http.StatusOK)
} else {
writer.WriteHeader(http.StatusNotFound)
}
case strings.HasPrefix(p, "GET /api/datasources"):
var dbs []string
for _, val := range data {
dbs = append(dbs, string(val))
}
_, _ = writer.Write([]byte("[" + strings.Join(dbs, ",") + "]"))
writer.WriteHeader(http.StatusOK)
case strings.HasPrefix(p, "DELETE /api/datasources/uid/"):
uid := strings.TrimPrefix(p, "DELETE /api/datasources/uid/")
if _, ok := data[uid]; ok {
delete(data, uid)
writer.WriteHeader(http.StatusOK)
} else {
writer.WriteHeader(http.StatusNotFound)
}
default:
writer.WriteHeader(http.StatusNotFound)
}
}))
})
AfterEach(func() {
Ω(k8s.ClearNamespace(context.Background(), singleton.KubeClient.Get(), config.ObservabilityNamespace)).To(Succeed())
mockServer.Close()
})
It("Test GrafanaDatasource API", func() {
s := &GrafanaDatasource{}
By("Test meta info")
By("Test meta info")
Ω(s.New()).To(Equal(&GrafanaDatasource{}))
Ω(s.GetObjectMeta()).To(Equal(&metav1.ObjectMeta{}))
Ω(s.NamespaceScoped()).To(BeFalse())
Ω(s.ShortNames()).To(ContainElement("gds"))
Ω(s.GetGroupVersionResource().GroupVersion()).To(Equal(GroupVersion))
Ω(s.GetGroupVersionResource().Resource).To(Equal(GrafanaDatasourceResource))
Ω(s.IsStorageVersion()).To(BeTrue())
Ω(s.NewList()).To(Equal(&GrafanaDatasourceList{}))
ctx := context.Background()
By("Create Grafana")
grafana := &grafanav1alpha1.Grafana{
ObjectMeta: metav1.ObjectMeta{Name: subresource.DefaultParentResourceName},
Spec: grafanav1alpha1.GrafanaSpec{
Endpoint: mockServer.URL,
Access: grafanav1alpha1.AccessCredential{Token: pointer.String("mock")},
},
}
_, err := (&grafanav1alpha1.Grafana{}).Create(ctx, grafana, nil, nil)
Ω(err).To(Succeed())
By("Test Create GrafanaDatasource")
_, err = s.Create(ctx, &GrafanaDatasource{
ObjectMeta: metav1.ObjectMeta{Name: "alpha"},
Spec: runtime.RawExtension{Raw: []byte(`{"id":0,"key":"val"}`)},
}, nil, nil)
Ω(err).To(Succeed())
_, err = s.Create(ctx, &GrafanaDatasource{
ObjectMeta: metav1.ObjectMeta{Name: "beta"},
Spec: runtime.RawExtension{Raw: []byte(`{"id":1,"key":"value"}`)},
}, nil, nil)
Ω(err).To(Succeed())
By("Test Update GrafanaDatasource")
_, _, err = s.Update(ctx, "beta", rest.DefaultUpdatedObjectInfo(&GrafanaDatasource{
ObjectMeta: metav1.ObjectMeta{Name: "beta"},
Spec: runtime.RawExtension{Raw: []byte(`{"id":1,"key":"v"}`)},
}), nil, nil, false, nil)
Ω(err).To(Succeed())
By("Test Get GrafanaDatasource")
obj, err := s.Get(ctx, "alpha", nil)
Ω(err).To(Succeed())
gdb, ok := obj.(*GrafanaDatasource)
Ω(ok).To(BeTrue())
Ω(gdb.Spec.Raw).To(Equal([]byte(`{"id":0,"key":"val","uid":"alpha"}`)))
By("Test List GrafanaDatasource")
objs, err := s.List(ctx, nil)
Ω(err).To(Succeed())
dbs, ok := objs.(*GrafanaDatasourceList)
Ω(ok).To(BeTrue())
Ω(len(dbs.Items)).To(Equal(2))
By("Test Delete GrafanaDatasource")
_, _, err = s.Delete(ctx, "alpha", nil, nil)
Ω(err).To(Succeed())
objs, err = s.List(ctx, nil)
Ω(err).To(Succeed())
dbs, ok = objs.(*GrafanaDatasourceList)
Ω(ok).To(BeTrue())
Ω(len(dbs.Items)).To(Equal(1))
By("Test GrafanaDatasource Printer")
_, err = s.ConvertToTable(ctx, obj, nil)
Ω(err).To(Succeed())
_, err = s.ConvertToTable(ctx, objs, nil)
Ω(err).To(Succeed())
})
})

View File

@ -0,0 +1,129 @@
/*
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 v1alpha1
import (
"context"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
"github.com/kubevela/pkg/util/apiserver"
"github.com/kubevela/pkg/util/singleton"
)
// GrafanaDatasource is a reflection api for Grafana Datasource
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type GrafanaDatasource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// +kubebuilder:pruning:PreserveUnknownFields
Spec runtime.RawExtension `json:"spec,omitempty"`
}
// GrafanaDatasourceList list for GrafanaDatasource
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type GrafanaDatasourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []GrafanaDatasource `json:"items"`
}
var _ resource.Object = &GrafanaDatasource{}
var _ rest.Getter = &GrafanaDatasource{}
var _ rest.CreaterUpdater = &GrafanaDatasource{}
var _ rest.Patcher = &GrafanaDatasource{}
var _ rest.GracefulDeleter = &GrafanaDatasource{}
var _ rest.Lister = &GrafanaDatasource{}
// GetObjectMeta returns the object meta reference.
func (in *GrafanaDatasource) GetObjectMeta() *metav1.ObjectMeta {
return &in.ObjectMeta
}
// NamespaceScoped returns if the object must be in a namespace.
func (in *GrafanaDatasource) NamespaceScoped() bool {
return false
}
// New returns a new instance of the resource
func (in *GrafanaDatasource) New() runtime.Object {
return &GrafanaDatasource{}
}
// Destroy .
func (in *GrafanaDatasource) Destroy() {}
// NewList return a new list instance of the resource
func (in *GrafanaDatasource) NewList() runtime.Object {
return &GrafanaDatasourceList{}
}
// GetGroupVersionResource returns the GroupVersionResource for this resource.
func (in *GrafanaDatasource) GetGroupVersionResource() schema.GroupVersionResource {
return GroupVersion.WithResource(GrafanaDatasourceResource)
}
// IsStorageVersion returns true if the object is also the internal version
func (in *GrafanaDatasource) IsStorageVersion() bool {
return true
}
// ShortNames delivers a list of short names for a resource.
func (in *GrafanaDatasource) ShortNames() []string {
return []string{"gds", "datasource", "datasources", "grafana-datasource", "grafana-datasources"}
}
// Get finds a resource in the storage by name and returns it.
func (in *GrafanaDatasource) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return NewGrafanaDatasourceClient(singleton.KubeClient.Get()).Get(ctx, name)
}
func (in *GrafanaDatasource) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
return obj, NewGrafanaDatasourceClient(singleton.KubeClient.Get()).Create(ctx, obj.(*GrafanaDatasource))
}
func (in *GrafanaDatasource) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (obj runtime.Object, _ bool, err error) {
cli := NewGrafanaDatasourceClient(singleton.KubeClient.Get())
if obj, err = cli.Get(ctx, name); err != nil {
return nil, false, err
}
if obj, err = objInfo.UpdatedObject(ctx, obj); err != nil {
return nil, false, err
}
return obj, false, cli.Update(ctx, obj.(*GrafanaDatasource))
}
func (in *GrafanaDatasource) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (obj runtime.Object, _ bool, err error) {
cli := NewGrafanaDatasourceClient(singleton.KubeClient.Get())
if obj, err = cli.Get(ctx, name); err != nil {
return nil, false, err
}
return obj, true, cli.Delete(ctx, obj.(*GrafanaDatasource))
}
func (in *GrafanaDatasource) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
if name := apiserver.GetMetadataNameInFieldSelectorFromInternalVersionListOptions(options); name != nil {
return NewGrafanaDatasourceClient(singleton.KubeClient.Get()).Get(ctx, *name)
}
return NewGrafanaDatasourceClient(singleton.KubeClient.Get()).List(ctx, apiserver.NewMatchingLabelSelectorFromInternalVersionListOptions(options))
}

View File

@ -0,0 +1,84 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
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.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GrafanaDatasource) DeepCopyInto(out *GrafanaDatasource) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDatasource.
func (in *GrafanaDatasource) DeepCopy() *GrafanaDatasource {
if in == nil {
return nil
}
out := new(GrafanaDatasource)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *GrafanaDatasource) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GrafanaDatasourceList) DeepCopyInto(out *GrafanaDatasourceList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]GrafanaDatasource, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDatasourceList.
func (in *GrafanaDatasourceList) DeepCopy() *GrafanaDatasourceList {
if in == nil {
return nil
}
out := new(GrafanaDatasourceList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *GrafanaDatasourceList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}

View File

@ -0,0 +1,33 @@
/*
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 apiserver
import (
"k8s.io/apiserver/pkg/server"
"github.com/kubevela/pkg/util/singleton"
)
// StartDefaultDynamicAPIServer run default dynamic apiserver in backend
func StartDefaultDynamicAPIServer(ctx server.PostStartHookContext) error {
DefaultDynamicAPIServer = NewDynamicAPIServer(
singleton.GenericAPIServer.Get(),
singleton.APIServerConfig.Get())
go StartDynamicResourceFactoryWithConfigMapInformer(ctx.StopCh)
//go StartDynamicResourceFactoryWithConfigMapInformer(ctx.StopCh)
return nil
}

View File

@ -0,0 +1,245 @@
/*
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 apiserver
import (
"path"
"sync"
"time"
"cuelang.org/go/pkg/strings"
"github.com/emicklei/go-restful/v3"
"golang.org/x/exp/slices"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
genericapi "k8s.io/apiserver/pkg/endpoints"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/server"
)
var DefaultDynamicAPIServer *DynamicAPIServer
type ResourceProvider interface {
rest.Storage
GetGroupVersion() schema.GroupVersion
GetGroupVersionKind() schema.GroupVersionKind
GetGroupVersionResource() schema.GroupVersionResource
}
type DynamicAPIServer struct {
server *server.GenericAPIServer
config *server.Config
scheme *runtime.Scheme
parameterCodec runtime.ParameterCodec
negotiatedSerializer runtime.NegotiatedSerializer
mu sync.Mutex
apiGroups map[string]*metav1.APIGroup
apiGroupVersions map[schema.GroupVersion]*genericapi.APIGroupVersion
apiGroupVersionHandlers map[schema.GroupVersion]*restful.WebService
}
func NewDynamicAPIServer(svr *server.GenericAPIServer, config *server.Config) *DynamicAPIServer {
s := &DynamicAPIServer{server: svr, config: config}
s.scheme = runtime.NewScheme()
s.parameterCodec = runtime.NewParameterCodec(s.scheme)
s.negotiatedSerializer = serializer.NewCodecFactory(s.scheme)
s.apiGroups = map[string]*metav1.APIGroup{}
s.apiGroupVersions = map[schema.GroupVersion]*genericapi.APIGroupVersion{}
metav1.AddToGroupVersion(s.scheme, metav1.Unversioned)
return s
}
func (in *DynamicAPIServer) removeWebService(prefix string) {
var toRemove *restful.WebService
for _, svc := range in.server.Handler.GoRestfulContainer.RegisteredWebServices() {
if svc.RootPath() == prefix {
toRemove = svc
break
}
}
if toRemove != nil {
_ = in.server.Handler.GoRestfulContainer.Remove(toRemove)
}
}
func NewGroupVersionForDiscovery(gv schema.GroupVersion) metav1.GroupVersionForDiscovery {
return metav1.GroupVersionForDiscovery{
GroupVersion: gv.String(),
Version: gv.Version,
}
}
func (in *DynamicAPIServer) AddGroupDiscovery(gv schema.GroupVersion) {
in.mu.Lock()
defer in.mu.Unlock()
gv4discovery := NewGroupVersionForDiscovery(gv)
apiGroup, exists := in.apiGroups[gv.Group]
if !exists {
apiGroup = &metav1.APIGroup{
Name: gv.Group,
Versions: []metav1.GroupVersionForDiscovery{},
PreferredVersion: gv4discovery,
}
}
if slices.Contains(apiGroup.Versions, gv4discovery) {
return
}
apiGroup.Versions = append(apiGroup.Versions, gv4discovery)
in.apiGroups[gv.Group] = apiGroup
in.server.DiscoveryGroupManager.RemoveGroup(gv.Group)
in.server.DiscoveryGroupManager.AddGroup(*apiGroup)
}
func (in *DynamicAPIServer) RemoveGroupDiscovery(gv schema.GroupVersion) {
in.mu.Lock()
defer in.mu.Unlock()
apiGroup, exists := in.apiGroups[gv.Group]
if !exists {
return
}
gv4discovery := NewGroupVersionForDiscovery(gv)
if idx := slices.Index(apiGroup.Versions, gv4discovery); idx >= 0 {
apiGroup.Versions = slices.Delete(apiGroup.Versions, idx, idx)
}
if len(apiGroup.Versions) > 0 && apiGroup.PreferredVersion == gv4discovery {
apiGroup.PreferredVersion = apiGroup.Versions[0]
}
if len(apiGroup.Versions) == 0 {
in.server.DiscoveryGroupManager.RemoveGroup(gv.Group)
delete(in.apiGroups, gv.Group)
return
}
in.apiGroups[gv.Group] = apiGroup
in.server.DiscoveryGroupManager.RemoveGroup(gv.Group)
in.server.DiscoveryGroupManager.AddGroup(*apiGroup)
}
func (in *DynamicAPIServer) AddGroupVersionResourceHandler(gvr schema.GroupVersionResource, storage rest.Storage) error {
in.mu.Lock()
defer in.mu.Unlock()
gv := gvr.GroupVersion()
apiGroupVersion, exists := in.apiGroupVersions[gv]
if !exists {
apiGroupVersion = &genericapi.APIGroupVersion{
Root: server.APIGroupPrefix,
Storage: map[string]rest.Storage{},
GroupVersion: gv,
MetaGroupVersion: nil,
ParameterCodec: in.parameterCodec,
Serializer: in.negotiatedSerializer,
Creater: in.scheme,
Convertor: in.scheme,
ConvertabilityChecker: in.scheme,
UnsafeConvertor: runtime.UnsafeObjectConvertor(in.scheme),
Defaulter: in.scheme,
Typer: in.scheme,
Namer: runtime.Namer(meta.NewAccessor()),
EquivalentResourceRegistry: in.server.EquivalentResourceRegistry,
Admit: in.config.AdmissionControl,
MinRequestTimeout: time.Duration(in.config.MinRequestTimeout) * time.Second,
MaxRequestBodyBytes: in.config.MaxRequestBodyBytes,
Authorizer: in.server.Authorizer,
}
}
apiGroupVersion.Storage[gvr.Resource] = storage
in.apiGroupVersions[gv] = apiGroupVersion
in.removeGroupVersionHandler(gv)
_, _, err := apiGroupVersion.InstallREST(in.server.Handler.GoRestfulContainer)
return err
}
func (in *DynamicAPIServer) RemoveGroupVersionResourceHandler(gvr schema.GroupVersionResource, storage rest.Storage) error {
in.mu.Lock()
defer in.mu.Unlock()
gv := gvr.GroupVersion()
apiGroupVersion, exists := in.apiGroupVersions[gv]
if !exists {
return nil
}
delete(apiGroupVersion.Storage, gvr.Resource)
in.removeGroupVersionHandler(gv)
if len(apiGroupVersion.Storage) == 0 {
delete(in.apiGroupVersions, gv)
return nil
}
_, _, err := apiGroupVersion.InstallREST(in.server.Handler.GoRestfulContainer)
return err
}
func (in *DynamicAPIServer) removeGroupVersionHandler(gv schema.GroupVersion) {
prefix := path.Join(server.APIGroupPrefix, gv.Group, gv.Version)
webservices := in.server.Handler.GoRestfulContainer.RegisteredWebServices()
if idx := slices.IndexFunc(webservices, func(ws *restful.WebService) bool {
return ws.RootPath() == prefix
}); idx >= 0 {
_ = in.server.Handler.GoRestfulContainer.Remove(webservices[idx])
}
}
func (in *DynamicAPIServer) AddResource(r ResourceProvider) error {
in.AddScheme(r.GetGroupVersionKind(), r)
in.AddGroupDiscovery(r.GetGroupVersion())
return in.AddGroupVersionResourceHandler(r.GetGroupVersionResource(), r)
}
func (in *DynamicAPIServer) RemoveResource(r ResourceProvider) error {
in.RemoveScheme(r.GetGroupVersionKind())
in.RemoveGroupDiscovery(r.GetGroupVersion())
return in.RemoveGroupVersionResourceHandler(r.GetGroupVersionResource(), r)
}
func (in *DynamicAPIServer) AddScheme(gvk schema.GroupVersionKind, storage rest.Storage) {
in.mu.Lock()
defer in.mu.Unlock()
in.scheme.AddKnownTypeWithName(gvk, storage.New())
metav1.AddToGroupVersion(in.scheme, gvk.GroupVersion())
if listStorage, ok := storage.(rest.Lister); ok {
in.scheme.AddKnownTypeWithName(gvk.GroupVersion().WithKind(gvk.Kind+"List"), listStorage.NewList())
}
}
func (in *DynamicAPIServer) RemoveScheme(gvk schema.GroupVersionKind) {
in.mu.Lock()
defer in.mu.Unlock()
newScheme := runtime.NewScheme()
for _gvk := range in.scheme.AllKnownTypes() {
_gvk.Kind = strings.TrimSuffix(_gvk.Kind, "List")
if _gvk == gvk {
continue
}
obj, err := in.scheme.New(_gvk)
if err != nil {
continue
}
newScheme.AddKnownTypeWithName(_gvk, obj)
if _gvk.Version != runtime.APIVersionInternal {
metav1.AddToGroupVersion(newScheme, _gvk.GroupVersion())
}
}
in.scheme = newScheme
in.parameterCodec = runtime.NewParameterCodec(in.scheme)
in.negotiatedSerializer = serializer.NewCodecFactory(in.scheme)
}

View File

@ -0,0 +1,103 @@
/*
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 apiserver
import (
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"github.com/kubevela/pkg/util/singleton"
"github.com/kubevela/prism/pkg/apis/dynamicresource"
)
const (
defaultNamespace = "vela-system"
encoderKey = "encoder"
decoderKey = "decoder"
)
func StartDynamicResourceFactoryWithConfigMapInformer(stopCh <-chan struct{}) {
factory := informers.NewSharedInformerFactoryWithOptions(
singleton.StaticClient.Get(), 0,
informers.WithNamespace(defaultNamespace))
informer := factory.Core().V1().ConfigMaps().Informer()
defer runtime.HandleCrash()
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
if err := addResource(obj); err != nil {
klog.Error(err)
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
if err := removeResource(oldObj); err != nil {
klog.Error(err)
}
if err := addResource(newObj); err != nil {
klog.Error(err)
}
},
DeleteFunc: func(obj interface{}) {
if err := removeResource(obj); err != nil {
klog.Error(err)
}
},
})
informer.Run(stopCh)
}
func handleResource(obj interface{}, action string, handler func(ResourceProvider) error) error {
var r ResourceProvider
var err error
switch o := obj.(type) {
case *corev1.ConfigMap:
r, err = newDynamicResourceFromConfigMap(o)
default:
return fmt.Errorf("cannot recognize %T type", obj)
}
if err != nil {
return err
}
if r == nil {
return nil
}
klog.Infof("Handle Resource %s.%s (%s)", r.GetGroupVersionResource().Resource, r.GetGroupVersion(), action)
return handler(r)
}
func addResource(obj interface{}) error {
return handleResource(obj, "Add", DefaultDynamicAPIServer.AddResource)
}
func removeResource(obj interface{}) error {
return handleResource(obj, "Remove", DefaultDynamicAPIServer.RemoveResource)
}
func newDynamicResourceFromConfigMap(cm *corev1.ConfigMap) (obj ResourceProvider, err error) {
encoderTemplate, encoderExists := cm.Data[encoderKey]
decoderTemplate, decoderExists := cm.Data[decoderKey]
if !encoderExists || !decoderExists {
return nil, nil
}
obj, err = dynamicresource.NewDynamicResourceWithCodec(encoderTemplate, decoderTemplate)
return obj, err
}

View File

@ -0,0 +1,138 @@
/*
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 apiserver_test
import (
"context"
"fmt"
"path"
"strings"
"testing"
"time"
"github.com/emicklei/go-restful/v3"
"github.com/kubevela/pkg/meta"
"github.com/kubevela/pkg/util/k8s"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"golang.org/x/exp/slices"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/discovery"
"k8s.io/apiserver/pkg/server"
"sigs.k8s.io/apiserver-runtime/pkg/builder"
"github.com/kubevela/pkg/util/singleton"
apiserver "github.com/kubevela/prism/pkg/dynamicapiserver"
_ "github.com/kubevela/prism/test/bootstrap"
)
func TestDynamicServer(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Test Dynamic Server")
}
func createConfigMapForDiscovery(group, version, kind string) *corev1.ConfigMap {
cm := &corev1.ConfigMap{}
resource := strings.ToLower(kind) + "s"
cm.SetName(fmt.Sprintf("%s.%s.%s", resource, group, version))
cm.SetNamespace("vela-system")
cm.Data = map[string]string{}
cm.Data["encoder"] = fmt.Sprintf(`
parameter: {
apiVersion: "%s/%s"
kind: "%s"
metadata: {...}
}
output: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: parameter.metadata
data: {}
}
`, group, version, kind)
cm.Data["decoder"] = fmt.Sprintf(`
parameter: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {...}
data: {...}
}
output: {
apiVersion: "%s/%s"
kind: "%s"
metadata: parameter.metadata
}
`, group, version, kind)
return cm
}
var _ = Describe("Test dynamic server", func() {
It("Test bootstrap and mutate spec", func() {
By("Bootstrap")
_ = k8s.EnsureNamespace(context.Background(), singleton.KubeClient.Get(), meta.NamespaceVelaSystem)
s := &builder.GenericAPIServer{
Handler: &server.APIServerHandler{
GoRestfulContainer: restful.NewContainer(),
},
DiscoveryGroupManager: discovery.NewRootAPIsHandler(nil, nil),
EquivalentResourceRegistry: runtime.NewEquivalentResourceRegistry(),
}
singleton.InitGenericAPIServer(s)
cfg := &server.RecommendedConfig{}
singleton.InitServerConfig(cfg)
stopCh := make(chan struct{})
defer close(stopCh)
_ = apiserver.StartDefaultDynamicAPIServer(server.PostStartHookContext{StopCh: stopCh})
By("Add Resource API")
ctx := context.Background()
cm1 := createConfigMapForDiscovery("test.oam.dev", "v1alpha1", "Test")
Ω(singleton.KubeClient.Get().Create(ctx, cm1)).To(Succeed())
Eventually(func(g Gomega) {
g.Ω(slices.IndexFunc(s.Handler.GoRestfulContainer.RegisteredWebServices(), func(_ws *restful.WebService) bool {
return _ws.RootPath() == path.Join(server.APIGroupPrefix, "test.oam.dev", "v1alpha1")
}) >= 0).Should(BeTrue())
}).WithTimeout(5 * time.Second).WithPolling(1 * time.Second).Should(Succeed())
By("Update & Delete Resource API")
Ω(singleton.KubeClient.Get().Delete(ctx, cm1)).To(Succeed())
cm2 := createConfigMapForDiscovery("next.test.oam.dev", "v1alpha1", "Test")
Ω(singleton.KubeClient.Get().Create(ctx, cm2)).To(Succeed())
cm2.Data = createConfigMapForDiscovery("new.test.oam.dev", "v1alpha1", "Test").Data
Ω(singleton.KubeClient.Get().Update(ctx, cm2)).To(Succeed())
cm3 := createConfigMapForDiscovery("next.new.test.oam.dev", "v1alpha1", "Test")
Ω(singleton.KubeClient.Get().Create(ctx, cm3)).To(Succeed())
Ω(singleton.KubeClient.Get().Delete(ctx, cm3)).To(Succeed())
Eventually(func(g Gomega) {
webservices := s.Handler.GoRestfulContainer.RegisteredWebServices()
g.Ω(slices.IndexFunc(webservices, func(_ws *restful.WebService) bool {
return _ws.RootPath() == path.Join(server.APIGroupPrefix, "test.oam.dev", "v1alpha1")
}) < 0).Should(BeTrue())
g.Ω(slices.IndexFunc(webservices, func(_ws *restful.WebService) bool {
return _ws.RootPath() == path.Join(server.APIGroupPrefix, "next.test.oam.dev", "v1alpha1")
}) < 0).Should(BeTrue())
g.Ω(slices.IndexFunc(webservices, func(_ws *restful.WebService) bool {
return _ws.RootPath() == path.Join(server.APIGroupPrefix, "new.test.oam.dev", "v1alpha1")
}) >= 0).Should(BeTrue())
g.Ω(slices.IndexFunc(webservices, func(_ws *restful.WebService) bool {
return _ws.RootPath() == path.Join(server.APIGroupPrefix, "new.next.test.oam.dev", "v1alpha1")
}) < 0).Should(BeTrue())
}).WithTimeout(15 * time.Second).WithPolling(1 * time.Second).Should(Succeed())
})
})

View File

@ -1,68 +0,0 @@
/*
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 singleton
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/server"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
var kubeConfig *rest.Config
var kubeClient client.Client
var restMapper meta.RESTMapper
// GetKubeConfig get kubernetes config
func GetKubeConfig() *rest.Config {
return kubeConfig
}
// SetKubeConfig set kubernetes config
func SetKubeConfig(cfg *rest.Config) {
kubeConfig = cfg
}
// GetKubeClient get kubernetes client
func GetKubeClient() client.Client {
return kubeClient
}
// SetKubeClient set kubernetes client
func SetKubeClient(cli client.Client) {
kubeClient = cli
}
// InitLoopbackClient init clients
func InitLoopbackClient(ctx server.PostStartHookContext) (err error) {
if kubeConfig, err = config.GetConfig(); err != nil {
return err
}
if restMapper, err = apiutil.NewDiscoveryRESTMapper(kubeConfig); err != nil {
return err
}
if kubeClient, err = client.New(kubeConfig, client.Options{
Scheme: scheme.Scheme,
Mapper: restMapper,
}); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,65 @@
/*
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 subresource
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
)
const (
// CompoundNameSeparator concatenate child resource name and parent resource name
CompoundNameSeparator = "@"
// DefaultParentResourceName indicates the default parent resource name to use when not explicitly specified
DefaultParentResourceName = "default"
)
// CompoundName the combination for resource name
type CompoundName struct {
ParentResourceName string
SubResourceName string
}
// String .
func (in *CompoundName) String() string {
return fmt.Sprintf("%s%s%s", in.SubResourceName, CompoundNameSeparator, in.ParentResourceName)
}
// NewCompoundName decode names into parent resource part and subresource part
func NewCompoundName(name string) *CompoundName {
if !strings.Contains(name, CompoundNameSeparator) {
return &CompoundName{ParentResourceName: DefaultParentResourceName, SubResourceName: name}
}
parts := strings.SplitN(name, CompoundNameSeparator, 2)
return &CompoundName{ParentResourceName: parts[1], SubResourceName: parts[0]}
}
// GetParentResourceNameFromLabelSelector retrieve parent resource key from label selector
func GetParentResourceNameFromLabelSelector(sel labels.Selector, parentResourceKey string) string {
requirements, _ := sel.Requirements()
for _, r := range requirements {
if r.Key() == parentResourceKey {
if r.Operator() == selection.Equals && len(r.Values().List()) == 1 {
return r.Values().List()[0]
}
}
}
return DefaultParentResourceName
}

View File

@ -0,0 +1,39 @@
/*
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 subresource
import (
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
)
func TestCompoundName(t *testing.T) {
require.Equal(t, "test@default", NewCompoundName("test").String())
require.Equal(t, "test@local", NewCompoundName("test@local").String())
}
func TestGetParentResourceNameFromLabelSelector(t *testing.T) {
sel := labels.NewSelector()
require.Equal(t, "default", GetParentResourceNameFromLabelSelector(sel, "key"))
r, err := labels.NewRequirement("key", selection.Equals, []string{"val"})
require.NoError(t, err)
sel = sel.Add(*r)
require.Equal(t, "val", GetParentResourceNameFromLabelSelector(sel, "key"))
}

View File

@ -0,0 +1,31 @@
/*
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 bootstrap
import (
"path/filepath"
"runtime"
"github.com/kubevela/pkg/util/test/bootstrap"
)
var (
_, fp, _, _ = runtime.Caller(0)
_ = bootstrap.InitKubeBuilderForTest(
bootstrap.WithCRDPath(filepath.Join(filepath.Dir(fp), "../testdata/crds")),
)
)