mirror of https://github.com/kubevela/prism.git
Compare commits
47 Commits
Author | SHA1 | Date |
---|---|---|
|
eee4e07950 | |
|
1ecdbb4aca | |
|
01dd83251a | |
|
88d23063b9 | |
|
c12eae92d0 | |
|
dc809e6c77 | |
|
b7e45f7f84 | |
|
cad8574718 | |
|
946eb7a2ce | |
|
eee3f1c2cc | |
|
d67ab30eb9 | |
|
8026c15cec | |
|
51eb282528 | |
|
fd6779756c | |
|
2aca265923 | |
|
6e77b01d61 | |
|
c431248cb0 | |
|
bb878067ac | |
|
42496db81b | |
|
bff60a0474 | |
|
6a25625e53 | |
|
faad0deebb | |
|
5dd2da483c | |
|
9463fc48de | |
|
6bf3ad33f8 | |
|
c3fb485060 | |
|
c752b9d821 | |
|
0b780bccf0 | |
|
b343320c2e | |
|
b2d7fec4ef | |
|
c3c6d996c2 | |
|
245a784aeb | |
|
6268967fe5 | |
|
dfe1d1910f | |
|
3cbc86010c | |
|
bdcaaa8f3c | |
|
5f395adaeb | |
|
21f02c3a7a | |
|
dba38778cd | |
|
47de5e92c5 | |
|
e0e88c8b42 | |
|
03f16c1944 | |
|
b22b06bdcd | |
|
c803cd0951 | |
|
94f1190f87 | |
|
15fe69a2ad | |
|
27c9883d98 |
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -12,7 +12,7 @@ on:
|
|||
- release-*
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.18'
|
||||
GO_VERSION: '1.19'
|
||||
|
||||
jobs:
|
||||
|
||||
|
|
|
@ -29,4 +29,6 @@ default.etcd/**
|
|||
|
||||
*.tgz
|
||||
index.yaml
|
||||
*.test
|
||||
*.test
|
||||
|
||||
hack/debug
|
96
README.md
96
README.md
|
@ -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
|
||||
|
||||

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

|
||||
|
||||
#### 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.
|
|
@ -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 }}
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"]
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -9,3 +9,4 @@ coverage:
|
|||
ignore:
|
||||
- "**/zz_generated.deepcopy.go"
|
||||
- "cmd/apiserver/*"
|
||||
- "test/**"
|
||||
|
|
174
go.mod
174
go.mod
|
@ -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
|
||||
)
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 118 KiB |
|
@ -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())
|
||||
})
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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())
|
||||
})
|
||||
})
|
|
@ -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.")
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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{}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
|
@ -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))
|
||||
})
|
||||
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
)
|
|
@ -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())
|
||||
})
|
||||
|
||||
})
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
)
|
|
@ -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())
|
||||
})
|
||||
|
||||
})
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"))
|
||||
}
|
|
@ -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")),
|
||||
)
|
||||
)
|
Loading…
Reference in New Issue