Compare commits

...

43 Commits

Author SHA1 Message Date
Yuxing Deng
eacc47482e refactor: UI resource logic
- Support embed api-ui resources
- The ui-path arg will be applied if provided. Also applied to api-ui resource files
2024-07-23 16:53:49 +08:00
Yuxing Deng
004e4751c8 fix: aliyun image is not built 2024-07-18 20:37:08 +08:00
Yuxing Deng
8bf22555dd fix: only release kube-explorer binary in release 2024-07-18 19:56:31 +08:00
Yuxing Deng
896e03e279 feat(ci): Added release step for tag
Skip compress process for PR build
Add commit message check
Add release-note
2024-07-18 17:19:41 +08:00
Yuxing Deng
979b4991fa feat: switch to GHA 2024-07-18 10:08:15 +08:00
Yuxing Deng
aec9926ed8 fix: Add dockerignore file to clean up each ci build 2024-07-17 15:02:39 +08:00
Yuxing Deng
2f3c1e6ab5 feat: Support specifying shell pod image and registry 2024-07-17 15:02:39 +08:00
Yuxing Deng
faa83722a0 refactor: No need to customize steve for explorer 2024-07-16 16:30:23 +08:00
Yuxing Deng
cd955243b6 dep(kube-explorer): Bump steve
Rebase to rancher v2.8.0 baseline
Upgrade ke-steve to ke/v0.4
Upgrade DASHBOARD UI version to v2.8.0
2024-01-05 15:00:39 +08:00
Yuxing Deng
2b39db9f07 fix(ci): Fix multi-arch build problem
Added build linux/arm64 binary to default ci
2023-11-15 16:30:57 +08:00
Yuxing Deng
4dc1acb1f2 feat(ci): Improve drone pipeline configuration
- Separate push and tag pipeline
- Use buildx to build and push multi-arch image
2023-11-15 16:09:58 +08:00
Yuxing Deng
989d087b99 No need to run hack_fs
As the embed fs packaged all files.
And bump upx version.
2023-11-13 16:29:48 +08:00
Yuxing Deng
c214e6ba6a Add windows amd64 support
Bump steve customized logic to fix serving embed assets problem
2023-11-13 15:46:03 +08:00
Yuxing Deng
390b11caef Bump steve
And upgrade builder image to v1.21.
2023-11-10 15:24:31 +08:00
niusmallnan
e016261c4b Bump steve and dashboard 2023-07-14 13:36:54 +08:00
niusmallnan
c43288964a Bump dashboard 2023-07-13 15:20:20 +08:00
niusmallnan
70e586976d Bump dashboard 2023-07-13 12:16:04 +08:00
niusmallnan
d0ce0e28bf Bump dashboard 2023-07-13 10:46:03 +08:00
niusmallnan
ad0a0c0cb3 Bump steve and dashboard for v2.7.5 2023-07-13 09:02:30 +08:00
niusmallnan
651d499086 Bumo bci 15.5 2023-07-11 11:35:10 +08:00
niusmallnan
8e592b1a3c Bump steve and dashboard for Rancher v2.7.2 2023-04-18 12:20:04 +08:00
niusmallnan
c1f5fda228 [CI SKIP] Update README for basic auth 2023-04-11 11:07:14 +08:00
niusmallnan
10e5323c95 [CI SKIP] Use ingress v1 for nginx-ingress basic auth demo 2023-04-10 17:18:22 +08:00
niusmallnan
ea49f9d3b4 [CI SKIP] Add basic-auth manifests for traefik v2 2023-04-10 17:09:30 +08:00
niusmallnan
b0b81ba87d [CI SKIP] switch to sslip.io for ingress demos 2023-04-10 16:20:43 +08:00
niusmallnan
e757347def Disable compress for darwin releases 2023-04-04 09:08:50 +08:00
niusmallnan
f4970b85a2 Bump steve and dashboard for Rancher v2.7.1 2023-03-20 15:13:04 +08:00
niusmallnan
bfae192748 Use --scanners instead of --security-checks 2023-03-20 11:24:18 +08:00
niusmallnan
3810cd702f Bump upx 4.0.2 2023-03-20 11:23:47 +08:00
niusmallnan
f898c559e0 Bump steve and dashboard 2022-12-20 15:16:25 +08:00
niusmallnan
f0effa7f09 Bump upx 4.0.1 2022-12-20 15:12:34 +08:00
niusmallnan
2838ceb34a Add image scan pipeline in drone 2022-12-09 09:29:18 +08:00
niusmallnan
40a972eeef Use BCI minimal image 2022-12-09 08:59:34 +08:00
niusmallnan
88c924a816 Use BCI image 2022-12-09 08:51:39 +08:00
niusmallnan
d24282849f Bump dashboard v2.6.9-kube-explorer-ui-rc2 2022-11-15 13:47:25 +08:00
niusmallnan
92aaca7407 Bump steve and dashboard based on Rancher v2.6.9 2022-10-19 11:58:11 +08:00
niusmallnan
c278dbb810 Bump golang 1.19 2022-10-19 11:57:10 +08:00
niusmallnan
5c2ecdfb97 Bump dashboard v2.6.9-rc4 [CI SKIP] 2022-10-12 15:58:46 +08:00
bagechashu
ecf6faba80 fix: text incorrect at deploy use kubectl
fix text incorrect
2022-10-07 13:24:45 +08:00
niusmallnan
a89b9b46bf Bump dashboard v2.6.9-rc1 [CI SKIP] 2022-09-26 11:10:32 +08:00
niusmallnan
30c0ceef73 Bump dashboard v2.6.8 2022-08-30 17:37:39 +08:00
Yuxing Deng
f6536c289e Update Dockerfile.dapper
Using `$GOPATH` instead of static path `/go`
2022-08-25 15:55:51 +08:00
niusmallnan
5347d02990 Bump dashboard 2022-08-22 13:27:48 +08:00
50 changed files with 3727 additions and 472 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
/bin
/dist
/internal/ui/ui

View File

@@ -1,294 +0,0 @@
---
kind: pipeline
name: default-amd64
platform:
os: linux
arch: amd64
steps:
- name: build
pull: default
image: rancher/dapper:v0.5.8
commands:
- dapper ci
privileged: true
volumes:
- name: docker
path: /var/run/docker.sock
when:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
event:
- push
- pull_request
- name: release
pull: default
image: rancher/dapper:v0.5.8
commands:
- dapper ci
privileged: true
environment:
CROSS: 1
volumes:
- name: docker
path: /var/run/docker.sock
when:
event:
- tag
- name: stage-binaries-head
image: rancher/dapper:v0.5.8
commands:
- "cp -r ./bin/kube-explorer ./package/"
when:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
event:
- push
- name: stage-binaries
image: rancher/dapper:v0.5.8
commands:
- "cp -r ./bin/kube-explorer-linux-amd64 ./package/kube-explorer"
when:
event:
- tag
- name: github_binary_release
pull: default
image: plugins/github-release
settings:
api_key:
from_secret: github_token
checksum:
- sha256
files:
- "bin/*"
title: "${DRONE_TAG}"
overwrite: true
when:
event:
- tag
- name: docker-publish-head
pull: default
image: plugins/docker
settings:
dockerfile: package/Dockerfile
context: package/
password:
from_secret: docker_password
repo: cnrancher/kube-explorer
tag: head-linux-amd64
username:
from_secret: docker_username
when:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
event:
- push
- name: docker-publish
pull: default
image: plugins/docker
settings:
dockerfile: package/Dockerfile
context: package/
password:
from_secret: docker_password
repo: cnrancher/kube-explorer
tag: ${DRONE_TAG}-linux-amd64
username:
from_secret: docker_username
when:
event:
- tag
volumes:
- name: docker
host:
path: /var/run/docker.sock
node:
instance: agent-amd64
trigger:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
- "refs/tags/*"
event:
exclude:
- promote
---
kind: pipeline
name: default-arm64
platform:
os: linux
arch: arm64
steps:
- name: build
pull: default
image: rancher/dapper:v0.5.8
commands:
- dapper ci
privileged: true
volumes:
- name: docker
path: /var/run/docker.sock
when:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
- "refs/tags/*"
event:
- push
- tag
- name: stage-binaries
image: rancher/dapper:v0.5.8
commands:
- "cp -r ./bin/* ./package/"
when:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
- "refs/tags/*"
event:
- push
- tag
- name: docker-publish-head
pull: default
image: plugins/docker
settings:
build_args:
- ARCH=arm64
dockerfile: package/Dockerfile
context: package/
password:
from_secret: docker_password
repo: cnrancher/kube-explorer
tag: head-linux-arm64
username:
from_secret: docker_username
when:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
event:
- push
- name: docker-publish
pull: default
image: plugins/docker
settings:
build_args:
- ARCH=arm64
dockerfile: package/Dockerfile
context: package/
password:
from_secret: docker_password
repo: cnrancher/kube-explorer
tag: ${DRONE_TAG}-linux-arm64
username:
from_secret: docker_username
when:
event:
- tag
volumes:
- name: docker
host:
path: /var/run/docker.sock
trigger:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
- "refs/tags/*"
event:
exclude:
- promote
node:
instance: agent-arm64
---
kind: pipeline
name: manifest
platform:
os: linux
arch: amd64
steps:
- name: push-manifest-head
image: plugins/manifest
settings:
ignore_missing: true
username:
from_secret: docker_username
password:
from_secret: docker_password
spec: manifest-head.tmpl
when:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
event:
- push
- name: push-manifest
image: plugins/manifest
settings:
ignore_missing: true
username:
from_secret: docker_username
password:
from_secret: docker_password
spec: manifest.tmpl
when:
event:
- tag
volumes:
- name: docker
host:
path: /var/run/docker.sock
node:
instance: agent-amd64
trigger:
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
- "refs/tags/*"
event:
exclude:
- promote
depends_on:
- default-amd64
- default-arm64
...

165
.drone_backup.yml Normal file
View File

@@ -0,0 +1,165 @@
type: docker
kind: pipeline
name: push
platform:
os: linux
arch: amd64
trigger:
event:
exclude:
- promote
include:
- push
- pull_request
volumes:
- name: docker
host:
path: /var/run/docker.sock
node:
instance: agent-amd64
steps:
- name: build
image: rancher/dapper:v0.6.0
commands:
- dapper ci
environment:
CROSS: "${DRONE_BUILD_EVENT}"
privileged: true
volumes:
- name: docker
path: /var/run/docker.sock
- name: image-scan-head
image: aquasec/trivy
commands:
- trivy image --no-progress --ignore-unfixed --severity HIGH,CRITICAL --scanners vuln --exit-code 1 cnrancher/kube-explorer:${DRONE_COMMIT:0:7}
volumes:
- name: docker
path: /var/run/docker.sock
when:
event:
- push
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
- name: install-buildx-support
image: tonistiigi/binfmt
privileged: true
entrypoint:
- /usr/bin/binfmt
command:
- --install
- all
when:
event:
- push
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
- name: docker-publish
image: thegeeklab/drone-docker-buildx
privileged: true
settings:
platforms: linux/amd64,linux/arm64/v8
dockerfile: package/Dockerfile
repo: cnrancher/kube-explorer
tag: latest
username:
from_secret: docker_username
password:
from_secret: docker_password
volumes:
- name: docker
path: /var/run/docker.sock
when:
event:
- push
ref:
include:
- "refs/heads/main"
- "refs/heads/v*"
---
type: docker
kind: pipeline
name: tag
platform:
os: linux
arch: amd64
trigger:
event:
exclude:
- promote
include:
- tag
ref:
include:
- "refs/tags/*"
volumes:
- name: docker
host:
path: /var/run/docker.sock
node:
instance: agent-amd64
steps:
- name: release
image: rancher/dapper:v0.6.0
commands:
- dapper ci
privileged: true
environment:
CROSS: "${DRONE_BUILD_EVENT}"
volumes:
- name: docker
path: /var/run/docker.sock
- name: install-buildx-support
image: tonistiigi/binfmt
privileged: true
entrypoint:
- /usr/bin/binfmt
command:
- --install
- all
- name: docker-publish
image: thegeeklab/drone-docker-buildx
privileged: true
settings:
platforms: linux/amd64,linux/arm64/v8
dockerfile: package/Dockerfile
repo: cnrancher/kube-explorer
tag: ${DRONE_TAG}
username:
from_secret: docker_username
password:
from_secret: docker_password
volumes:
- name: docker
path: /var/run/docker.sock
- name: github_binary_release
image: plugins/github-release
settings:
api_key:
from_secret: github_token
checksum:
- sha256
files:
- "bin/*"
title: "${DRONE_TAG}"
overwrite: true

21
.github/workflows/pr.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: pull request
on:
pull_request:
types:
- opened
- reopened
- synchronize
jobs:
pr-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Commitsar check
uses: aevea/commitsar@v0.20.2
- name: Build to test
env:
SKIP_COMPRESS: "true"
run: make ci

81
.github/workflows/push.yaml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Push to Master
on:
push:
branches:
- main
tags:
- 'v*.*.*' # Matches any tag that starts with 'v' and follows semantic versioning
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to Aliyun ACR
uses: docker/login-action@v3
with:
registry: registry.cn-shenzhen.aliyuncs.com
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_TOKEN }}
if: ${{ vars.ALIYUN == 'true' }}
- name: Login to Dockerhub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: CI
if: startsWith(github.ref, 'refs/heads/')
env:
CROSS: push
run: make github_ci
- name: CI
if: startsWith(github.ref, 'refs/tags/')
env:
CROSS: tag
run: |
make github_ci
make release-note
- name: Prepare for packaging image
run: cp dist/* package/
- name: Set docker iamge name
id: image-name
env:
REPO_OVERRIDE: ${{ vars.REPO || 'cnrancher' }}
IMAGE_OVERRIDE: ${{ vars.IMAGE || 'kube-explorer' }}
run: |
tag_name=latest;
if [[ ${GITHUB_REF} == refs/tags/* ]]; then tag_name=${GITHUB_REF#refs/tags/}; fi;
echo "image_name=${REPO_OVERRIDE}/${IMAGE_OVERRIDE}:${tag_name}" >> $GITHUB_OUTPUT;
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build to Dockerhub
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64/v8
tags: "${{ steps.image-name.outputs.image_name }}"
context: package
push: true
- name: Build to Aliyun
uses: docker/build-push-action@v6
with:
tags: registry.cn-shenzhen.aliyuncs.com/${{ steps.image-name.outputs.image_name }}
context: package
push: true
if: ${{ vars.ALIYUN == 'true' }}
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: dist/kube-explorer-*
body_path: dist/release-note
draft: true

4
.gitignore vendored
View File

@@ -19,3 +19,7 @@
/dist
/build
*.swp
/.vscode
/vendor
/internal/ui/ui/

68
.golangci.json Normal file
View File

@@ -0,0 +1,68 @@
{
"linters": {
"disable-all": true,
"enable": [
"govet",
"revive",
"goimports",
"misspell",
"ineffassign",
"gofmt"
]
},
"linters-settings": {
"govet": {
"check-shadowing": false
},
"gofmt": {
"simplify": false
}
},
"run": {
"skip-dirs": [
"vendor",
"tests",
"pkg/client",
"pkg/generated",
"scripts"
],
"tests": false,
"timeout": "10m"
},
"issues": {
"exclude-rules": [
{
"linters": "govet",
"text": "^(nilness|structtag)"
},
{
"path":"pkg/apis/management.cattle.io/v3/globaldns_types.go",
"text":".*lobalDns.*"
},
{
"path": "pkg/apis/management.cattle.io/v3/zz_generated_register.go",
"text":".*lobalDns.*"
},
{
"path":"pkg/apis/management.cattle.io/v3/zz_generated_list_types.go",
"text":".*lobalDns.*"
},
{
"linters": "revive",
"text": "should have comment"
},
{
"linters": "revive",
"text": "should be of the form"
},
{
"linters": "revive",
"text": "by other packages, and that stutters"
},
{
"linters": "typecheck",
"text": "imported but not used as apierrors"
}
]
}
}

View File

@@ -1,31 +1,29 @@
FROM golang:1.17
FROM aevea/release-notary:0.9.2 as tools
FROM registry.suse.com/bci/golang:1.22
ARG PROXY
ARG GOPROXY
ARG DAPPER_HOST_ARCH
ENV HOST_ARCH=${DAPPER_HOST_ARCH} ARCH=${DAPPER_HOST_ARCH}
ENV https_proxy=${PROXY} \
http_proxy=${PROXY}
RUN apt-get update && \
apt-get install -y ca-certificates git wget curl xz-utils && \
rm -f /bin/sh && ln -s /bin/bash /bin/sh && \
curl -sL https://github.com/upx/upx/releases/download/v3.96/upx-3.96-${ARCH}_linux.tar.xz | tar xvJf - --strip-components=1 -C /tmp && \
RUN zypper -n install ca-certificates git-core wget curl unzip tar vim less file xz
RUN zypper install -y -f docker
ENV UPX_VERSION 4.2.1
RUN curl -sL https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-${ARCH}_linux.tar.xz | tar xvJf - --strip-components=1 -C /tmp && \
mv /tmp/upx /usr/bin/
RUN if [ "${ARCH}" == "amd64" ]; then \
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.43.0; \
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.54.2; \
fi
COPY --from=tools /app/release-notary /usr/local/bin/
ENV CATTLE_DASHBOARD_UI_VERSION="v2.8.0-kube-explorer-ui-rc3"
ENV CATTLE_API_UI_VERSION="1.1.11"
ENV DOCKER_URL_amd64=https://get.docker.com/builds/Linux/x86_64/docker-1.10.3 \
DOCKER_URL_arm=https://github.com/rancher/docker/releases/download/v1.10.3-ros1/docker-1.10.3_arm \
DOCKER_URL_arm64=https://github.com/rancher/docker/releases/download/v1.10.3-ros1/docker-1.10.3_arm64 \
DOCKER_URL=DOCKER_URL_${ARCH}
RUN wget -O - ${!DOCKER_URL} > /usr/bin/docker && chmod +x /usr/bin/docker
ENV GIT_COMMIT="26e14afc0b652b0363fc38e05ef28aa99d26694c" \
GIT_BRANCH="ke/v0.2" \
GIT_SOURCE="/go/src/github.com/rancher/steve" \
CATTLE_DASHBOARD_UI_VERSION="v2.6.7-kube-explorer-ui-rc1"
ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS
ENV DAPPER_SOURCE /opt/kube-explorer
ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS GOPROXY SKIP_COMPRESS GITHUB_REPOSITORY GITHUB_TOKEN
ENV DAPPER_SOURCE /go/src/github.com/cnrancher/kube-explorer
ENV DAPPER_OUTPUT ./bin ./dist
ENV DAPPER_DOCKER_SOCKET true
ENV DAPPER_RUN_ARGS "-v ke-pkg:/go/pkg -v ke-cache:/root/.cache/go-build --privileged"

12
deploy/kubectl/README.md Normal file
View File

@@ -0,0 +1,12 @@
## Access Control Via Basic Auth
Deploy the kube-explorer workload:
```
kubectl create -f .
```
Configure for different IngressClass:
- [Nginx Ingress](./nginx-auth)
- [Traefik Ingress](./traefik-v2-auth)

View File

@@ -1,4 +1,4 @@
## Traefik Auth
## Ingress-Nginx Basic Auth
This can be used in the cluster which uses the nginx-ingress.
@@ -13,9 +13,9 @@ htpasswd -nb username password | base64
To install this mode, just run this script:
```
kubectl apply -f ./secret.yaml
export MY_XIP_IO=$(curl -sL ipinfo.io/ip)
envsubst < ./ingress.yaml.tpl | kubectl apply -f -
kubectl create -f ./secret.yaml
export MY_IP=$(curl -sL ipinfo.io/ip)
envsubst < ./ingress.yaml.tpl | kubectl create -f -
```
For more infos: https://kubernetes.github.io/ingress-nginx/examples/auth/basic/

View File

@@ -1,8 +1,8 @@
# Note: please replace the host first
# To use xip.io: http://xip.io/
# To use sslip.io: https://sslip.io/
# To get your public IP: curl ipinfo.io/ip
apiVersion: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kube-explorer
@@ -10,16 +10,18 @@ metadata:
labels:
app: kube-explorer
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: kube-explorer
nginx.ingress.kubernetes.io/auth-realm: 'Authentication Required - kube-explorer'
spec:
rules:
- host: "${MY_XIP_IO}.xip.io"
- host: "${MY_IP}.sslip.io"
http:
paths:
- path: /
pathType: Prefix
backend:
serviceName: kube-explorer
servicePort: 8989
service:
name: kube-explorer
port:
number: 8989

View File

@@ -13,9 +13,9 @@ htpasswd -nb username password | base64
To install this mode, just run this script:
```
kubectl apply -f ./secret.yaml
export MY_XIP_IO=$(curl -sL ipinfo.io/ip)
envsubst < ./ingress.yaml.tpl | kubectl apply -f -
kubectl create -f ./secret.yaml
export MY_IP=$(curl -sL ipinfo.io/ip)
envsubst < ./ingress.yaml.tpl | kubectl create -f -
```
For more infos: https://doc.traefik.io/traefik/v1.7/configuration/backends/kubernetes/

View File

@@ -1,5 +1,5 @@
# Note: please replace the host first
# To use xip.io: http://xip.io/
# To use sslip.io: https://sslip.io/
# To get your public IP: curl ipinfo.io/ip
apiVersion: networking.k8s.io/v1beta1
@@ -16,7 +16,7 @@ metadata:
ingress.kubernetes.io/auth-remove-header: "true"
spec:
rules:
- host: "${MY_XIP_IO}.xip.io"
- host: "${MY_IP}.sslip.io"
http:
paths:
- path: /

View File

@@ -0,0 +1,21 @@
## Traefik Auth
This can be used in K3s, as K3s use traefik as the default ingress class.
We use `basic-auth` to control the access of kube-explorer. The auth token is stored in the secret.
The default user is `niusmallnan`, and password is `dagedddd`. You can replace to another value with `htpasswd` tool.
```
htpasswd -nb username password | base64
```
To install this mode, just run this script:
```
kubectl create -f ./middleware.yaml
export MY_IP=$(curl -sL ipinfo.io/ip)
envsubst < ./ingress.yaml.tpl | kubectl create -f -
```
For more infos: https://doc.traefik.io/traefik/middlewares/http/basicauth/

View File

@@ -0,0 +1,25 @@
# Note: please replace the host first
# To use sslip.io.io: https://sslip.io.io/
# To get your public IP: curl ipinfo.io/ip
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kube-explorer
namespace: kube-system
labels:
app: kube-explorer
annotations:
traefik.ingress.kubernetes.io/router.middlewares: kube-system-kube-explorer@kubernetescrd
spec:
rules:
- host: "${MY_IP}.sslip.io"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kube-explorer
port:
number: 8989

View File

@@ -0,0 +1,28 @@
# The definitions below require the definitions for the Middleware and IngressRoute kinds.
# https://doc.traefik.io/traefik/reference/dynamic-configuration/kubernetes-crd/#definitions
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: kube-explorer
namespace: kube-system
labels:
app: kube-explorer
spec:
basicAuth:
secret: kube-explorer
removeHeader: true
---
# To create an encoded user:password pair, the following command can be used:
# htpasswd -nb user password | base64
apiVersion: v1
kind: Secret
metadata:
name: kube-explorer
namespace: kube-system
labels:
app: kube-explorer
data:
auth: bml1c21hbGxuYW46JGFwcjEkbDdUZjJOdWskbmNXajYubHYvMGNkcXM0NFoyelVQLgoK
type: Opaque

117
go.mod Normal file
View File

@@ -0,0 +1,117 @@
module github.com/cnrancher/kube-explorer
go 1.22.0
replace k8s.io/client-go => k8s.io/client-go v0.30.1
require (
github.com/gorilla/mux v1.8.1
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146
github.com/rancher/steve v0.0.0-20240709130809-47871606146c
github.com/rancher/wrangler/v3 v3.0.0
github.com/sirupsen/logrus v1.9.3
github.com/urfave/cli v1.22.15
golang.org/x/text v0.14.0
k8s.io/api v0.30.1
k8s.io/apimachinery v0.30.1
k8s.io/apiserver v0.30.1
k8s.io/client-go v12.0.0+incompatible
)
require (
github.com/adrg/xdg v0.4.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rancher/dynamiclistener v0.6.0-rc2 // indirect
github.com/rancher/kubernetes-provider-detector v0.1.5 // indirect
github.com/rancher/lasso v0.0.0-20240705194423-b2a060d103c1 // indirect
github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9 // indirect
github.com/rancher/remotedialer v0.3.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/urfave/cli/v2 v2.27.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/sdk v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.30.1 // indirect
k8s.io/component-base v0.30.1 // indirect
k8s.io/klog v1.0.0 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/kube-aggregator v0.30.1 // indirect
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.49.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.29.10 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 // indirect
sigs.k8s.io/cli-utils v0.35.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

2003
go.sum Normal file

File diff suppressed because it is too large Load Diff

35
internal/config/flags.go Normal file
View File

@@ -0,0 +1,35 @@
package config
import (
"github.com/urfave/cli"
)
var InsecureSkipTLSVerify bool
var SystemDefaultRegistry string
var APIUIVersion = "1.1.11"
var ShellPodImage string
func Flags() []cli.Flag {
return []cli.Flag{
cli.BoolFlag{
Name: "insecure-skip-tls-verify",
Destination: &InsecureSkipTLSVerify,
},
cli.StringFlag{
Name: "system-default-registry",
Destination: &SystemDefaultRegistry,
},
cli.StringFlag{
Name: "pod-image",
Destination: &ShellPodImage,
Value: "rancher/shell:v0.2.1-rc.7",
},
cli.StringFlag{
Name: "apiui-version",
Hidden: true,
Destination: &APIUIVersion,
Value: APIUIVersion,
},
}
}

11
internal/config/steve.go Normal file
View File

@@ -0,0 +1,11 @@
package config
import (
"github.com/rancher/steve/pkg/debug"
stevecli "github.com/rancher/steve/pkg/server/cli"
)
var (
Steve stevecli.Config
Debug debug.Config
)

View File

@@ -0,0 +1,80 @@
package cluster
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/podimpersonation"
"github.com/rancher/steve/pkg/resources/cluster"
"github.com/rancher/steve/pkg/server"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func Register(_ context.Context, server *server.Server, displayName string) error {
cg := server.ClientFactory
shell := &shell{
cg: cg,
namespace: shellPodNS,
impersonator: podimpersonation.New("shell", cg, time.Hour, getShellPodImage),
}
clusterSchema := server.BaseSchemas.LookupSchema("management.cattle.io.cluster")
if clusterSchema == nil {
return errors.New("failed to find management.cattle.io.cluster in base schema")
}
if clusterSchema.LinkHandlers == nil {
clusterSchema.LinkHandlers = make(map[string]http.Handler)
}
clusterSchema.LinkHandlers["shell"] = shell
clusterSchema.Store = func() types.Store {
return &displaynameWrapper{Store: clusterSchema.Store, displayName: displayName}
}()
return nil
}
type displaynameWrapper struct {
types.Store
displayName string
}
func (s *displaynameWrapper) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
obj, err := s.Store.ByID(apiOp, schema, id)
if err != nil {
return obj, err
}
if obj.ID != "local" {
return obj, nil
}
if c, ok := obj.Object.(*cluster.Cluster); ok {
c.Spec.DisplayName = getDisplayNameWithContext(s.displayName)
}
return obj, nil
}
func (s *displaynameWrapper) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
rtn, err := s.Store.List(apiOp, schema)
if err != nil {
return rtn, err
}
for _, obj := range rtn.Objects {
if obj.ID != "local" {
continue
}
if c, ok := obj.Object.(*cluster.Cluster); ok {
c.Spec.DisplayName = getDisplayNameWithContext(s.displayName)
}
}
return rtn, nil
}
func getDisplayNameWithContext(CurrentKubeContext string) string {
if CurrentKubeContext != "" {
return fmt.Sprintf("%s Cluster", cases.Title(language.English).String(CurrentKubeContext))
}
return "Local Cluster"
}

View File

@@ -0,0 +1,162 @@
package cluster
import (
"context"
"fmt"
"net/http"
"net/http/httputil"
"time"
"github.com/cnrancher/kube-explorer/internal/config"
"github.com/rancher/steve/pkg/podimpersonation"
"github.com/rancher/steve/pkg/stores/proxy"
"github.com/rancher/wrangler/v3/pkg/schemas/validation"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
)
var (
shellPodNS = "kube-system"
)
type shell struct {
namespace string
impersonator *podimpersonation.PodImpersonation
cg proxy.ClientGetter
}
func (s *shell) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ctx, user, client, err := s.contextAndClient(req)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
pod, err := s.impersonator.CreatePod(ctx, user, s.createPod(), &podimpersonation.PodOptions{
Wait: true,
})
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
client.CoreV1().Pods(pod.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{})
s.impersonator.DeleteRole(ctx, *pod)
}()
s.proxyRequest(rw, req, pod, client)
}
func (s *shell) proxyRequest(rw http.ResponseWriter, req *http.Request, pod *v1.Pod, client kubernetes.Interface) {
attachURL := client.CoreV1().RESTClient().
Get().
Namespace(pod.Namespace).
Resource("pods").
Name(pod.Name).
SubResource("exec").
VersionedParams(&v1.PodExecOptions{
Stdin: true,
Stdout: true,
Stderr: true,
TTY: true,
Container: "shell",
Command: []string{"welcome"},
}, scheme.ParameterCodec).URL()
httpClient := client.CoreV1().RESTClient().(*rest.RESTClient).Client
p := httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL = attachURL
req.Host = attachURL.Host
delete(req.Header, "Impersonate-Group")
delete(req.Header, "Impersonate-User")
delete(req.Header, "Authorization")
delete(req.Header, "Cookie")
},
Transport: httpClient.Transport,
FlushInterval: time.Millisecond * 100,
}
p.ServeHTTP(rw, req)
}
func (s *shell) contextAndClient(req *http.Request) (context.Context, user.Info, kubernetes.Interface, error) {
ctx := req.Context()
client, err := s.cg.AdminK8sInterface()
if err != nil {
return ctx, nil, nil, err
}
user, ok := request.UserFrom(ctx)
if !ok {
return ctx, nil, nil, validation.Unauthorized
}
return ctx, user, client, nil
}
func (s *shell) createPod() *v1.Pod {
return &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "dashboard-shell-",
Namespace: s.namespace,
},
Spec: v1.PodSpec{
TerminationGracePeriodSeconds: new(int64),
RestartPolicy: v1.RestartPolicyNever,
NodeSelector: map[string]string{
"kubernetes.io/os": "linux",
},
Tolerations: []v1.Toleration{
{
Key: "cattle.io/os",
Operator: "Equal",
Value: "linux",
Effect: "NoSchedule",
},
{
Key: "node-role.kubernetes.io/controlplane",
Operator: "Equal",
Value: "true",
Effect: "NoSchedule",
},
{
Key: "node-role.kubernetes.io/etcd",
Operator: "Equal",
Value: "true",
Effect: "NoExecute",
},
},
Containers: []v1.Container{
{
Name: "shell",
TTY: true,
Stdin: true,
StdinOnce: true,
Env: []v1.EnvVar{
{
Name: "KUBECONFIG",
Value: "/home/shell/.kube/config",
},
},
Image: getShellPodImage(),
ImagePullPolicy: v1.PullIfNotPresent,
},
},
},
}
}
func getShellPodImage() string {
if config.SystemDefaultRegistry == "" {
return config.ShellPodImage
}
return fmt.Sprintf("%s/%s", config.SystemDefaultRegistry, config.ShellPodImage)
}

101
internal/server/config.go Normal file
View File

@@ -0,0 +1,101 @@
package server
import (
"context"
"net/http"
"strings"
"github.com/rancher/apiserver/pkg/types"
steveauth "github.com/rancher/steve/pkg/auth"
"github.com/rancher/steve/pkg/schema"
"github.com/rancher/steve/pkg/server"
"github.com/rancher/steve/pkg/server/cli"
"github.com/rancher/steve/pkg/server/router"
"github.com/rancher/wrangler/v3/pkg/kubeconfig"
"github.com/rancher/wrangler/v3/pkg/ratelimit"
"github.com/cnrancher/kube-explorer/internal/config"
"github.com/cnrancher/kube-explorer/internal/resources/cluster"
"github.com/cnrancher/kube-explorer/internal/ui"
"github.com/cnrancher/kube-explorer/internal/version"
)
func ToServer(ctx context.Context, c *cli.Config, sqlCache bool) (*server.Server, error) {
var (
auth steveauth.Middleware
)
restConfig, err := kubeconfig.GetNonInteractiveClientConfigWithContext(c.KubeConfig, c.Context).ClientConfig()
if err != nil {
return nil, err
}
restConfig.RateLimiter = ratelimit.None
restConfig.Insecure = config.InsecureSkipTLSVerify
if restConfig.Insecure {
restConfig.CAData = nil
restConfig.CAFile = ""
}
if c.WebhookConfig.WebhookAuthentication {
auth, err = c.WebhookConfig.WebhookMiddleware()
if err != nil {
return nil, err
}
}
controllers, err := server.NewController(restConfig, nil)
if err != nil {
return nil, err
}
ui, apiui := ui.New(&ui.Options{
ReleaseSetting: version.IsRelease,
Path: func() string { return c.UIPath },
})
steveServer, err := server.New(ctx, restConfig, &server.Options{
AuthMiddleware: auth,
Controllers: controllers,
Next: ui,
SQLCache: sqlCache,
// router needs to hack here
Router: func(h router.Handlers) http.Handler {
return rewriteLocalCluster(router.Routes(h))
},
})
if err != nil {
return nil, err
}
steveServer.APIServer.CustomAPIUIResponseWriter(apiui.CSS(), apiui.JS(), func() string { return config.APIUIVersion })
// registrer local cluster
if err := cluster.Register(ctx, steveServer, c.Context); err != nil {
return steveServer, err
}
// wrap default store
steveServer.SchemaFactory.AddTemplate(schema.Template{
Customize: func(a *types.APISchema) {
if a.Store == nil {
return
}
a.Store = &deleteOptionStore{
Store: a.Store,
}
},
})
return steveServer, controllers.Start(ctx)
}
func rewriteLocalCluster(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if strings.HasPrefix(req.URL.Path, "/k8s/clusters/local") {
req.URL.Path = strings.TrimPrefix(req.URL.Path, "/k8s/clusters/local")
if req.URL.Path == "" {
req.URL.Path = "/"
}
}
next.ServeHTTP(rw, req)
})
}

View File

@@ -0,0 +1,16 @@
package server
import (
"github.com/rancher/apiserver/pkg/types"
)
type deleteOptionStore struct {
types.Store
}
func (s *deleteOptionStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
query := apiOp.Request.URL.Query()
query.Add("propagationPolicy", "Background")
apiOp.Request.URL.RawQuery = query.Encode()
return s.Store.Delete(apiOp, schema, id)
}

55
internal/ui/apiui.go Normal file
View File

@@ -0,0 +1,55 @@
package ui
import "github.com/rancher/apiserver/pkg/writer"
type APIUI struct {
offline StringSetting
release BoolSetting
embed bool
}
func apiUI(opt *Options) APIUI {
var rtn = APIUI{
offline: opt.Offline,
release: opt.ReleaseSetting,
embed: true,
}
if rtn.offline == nil {
rtn.offline = StaticSetting("dynamic")
}
if rtn.release == nil {
rtn.release = StaticSetting(false)
}
for _, file := range []string{
"ui/api-ui/ui.min.css",
"ui/api-ui/ui.min.js",
} {
if _, err := staticContent.Open(file); err != nil {
rtn.embed = false
break
}
}
return rtn
}
func (a APIUI) content(name string) writer.StringGetter {
return func() (rtn string) {
switch a.offline() {
case "dynamic":
if !a.release() && !a.embed {
return ""
}
case "false":
return ""
}
return name
}
}
func (a APIUI) CSS() writer.StringGetter {
return a.content("/api-ui/ui.min.css")
}
func (a APIUI) JS() writer.StringGetter {
return a.content("/api-ui/ui.min.js")
}

View File

@@ -0,0 +1,24 @@
package content
import (
"io/fs"
"net/http"
)
type fsFunc func(name string) (fs.File, error)
func (f fsFunc) Open(name string) (fs.File, error) {
return f(name)
}
type fsContent interface {
ToFileServer(basePaths ...string) http.Handler
Open(name string) (fs.File, error)
}
type Handler interface {
ServeAssets(middleware func(http.Handler) http.Handler, hext http.Handler) http.Handler
ServeFaviconDashboard() http.Handler
GetIndex() ([]byte, error)
Refresh()
}

View File

@@ -0,0 +1,97 @@
package content
import (
"bytes"
"crypto/tls"
"errors"
"io"
"net/http"
"sync"
)
const (
defaultIndex = "https://releases.rancher.com/dashboard/latest/index.html"
)
func NewExternal(getIndex func() string) Handler {
return &externalIndexHandler{
getIndexFunc: getIndex,
}
}
var (
insecureClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
_ Handler = &externalIndexHandler{}
)
type externalIndexHandler struct {
sync.RWMutex
getIndexFunc func() string
current string
downloadSuccess *bool
}
func (u *externalIndexHandler) ServeAssets(_ func(http.Handler) http.Handler, next http.Handler) http.Handler {
return next
}
func (u *externalIndexHandler) ServeFaviconDashboard() http.Handler {
return http.NotFoundHandler()
}
func (u *externalIndexHandler) GetIndex() ([]byte, error) {
if u.canDownload() {
var buffer bytes.Buffer
if err := serveIndex(&buffer, u.current); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
return nil, errors.New("external index is not available")
}
func serveIndex(resp io.Writer, url string) error {
r, err := insecureClient.Get(url)
if err != nil {
return err
}
defer r.Body.Close()
_, err = io.Copy(resp, r.Body)
return err
}
func (u *externalIndexHandler) canDownload() bool {
u.RLock()
rtn := u.downloadSuccess
u.RUnlock()
if rtn != nil {
return *rtn
}
return u.refresh()
}
func (u *externalIndexHandler) refresh() bool {
u.Lock()
defer u.RUnlock()
u.current = u.getIndexFunc()
if u.current == "" {
u.current = defaultIndex
}
t := serveIndex(io.Discard, u.current) == nil
u.downloadSuccess = &t
return t
}
func (u *externalIndexHandler) Refresh() {
_ = u.refresh()
}

71
internal/ui/content/fs.go Normal file
View File

@@ -0,0 +1,71 @@
package content
import (
"io"
"net/http"
"path/filepath"
"sync"
)
var _ Handler = &handler{}
func newFS(content fsContent) Handler {
return &handler{
content: content,
cacheFS: &sync.Map{},
}
}
type handler struct {
content fsContent
cacheFS *sync.Map
}
func (h *handler) pathExist(path string) bool {
_, err := h.content.Open(path)
return err == nil
}
func (h *handler) serveContent(basePaths ...string) http.Handler {
key := filepath.Join(basePaths...)
if rtn, ok := h.cacheFS.Load(key); ok {
return rtn.(http.Handler)
}
rtn := h.content.ToFileServer(basePaths...)
h.cacheFS.Store(key, rtn)
return rtn
}
func (h *handler) Refresh() {
h.cacheFS.Range(func(key, _ any) bool {
h.cacheFS.Delete(key)
return true
})
}
func (h *handler) ServeAssets(middleware func(http.Handler) http.Handler, next http.Handler) http.Handler {
assets := middleware(h.serveContent())
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if h.pathExist(r.URL.Path) {
assets.ServeHTTP(w, r)
} else {
next.ServeHTTP(w, r)
}
})
}
func (h *handler) ServeFaviconDashboard() http.Handler {
return h.serveContent("dashboard")
}
func (h *handler) GetIndex() ([]byte, error) {
path := filepath.Join("dashboard", "index.html")
f, err := h.content.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}

View File

@@ -0,0 +1,43 @@
package content
import (
"embed"
"io/fs"
"net/http"
"path/filepath"
)
func NewEmbedded(staticContent embed.FS, prefix string) Handler {
return newFS(&embedFS{
pathPrefix: prefix,
staticContent: staticContent,
})
}
var _ fsContent = &embedFS{}
type embedFS struct {
pathPrefix string
staticContent embed.FS
}
// Open implements fsContent.
func (e *embedFS) Open(name string) (fs.File, error) {
return e.staticContent.Open(joinEmbedFilepath(e.pathPrefix, name))
}
// ToFileServer implements fsContent.
func (e *embedFS) ToFileServer(basePaths ...string) http.Handler {
handler := fsFunc(func(name string) (fs.File, error) {
assetPath := joinEmbedFilepath(joinEmbedFilepath(basePaths...), name)
return e.Open(assetPath)
})
return http.FileServer(http.FS(handler))
}
func (e *embedFS) Refresh() error { return nil }
func joinEmbedFilepath(paths ...string) string {
return filepath.ToSlash(filepath.Join(paths...))
}

View File

@@ -0,0 +1,41 @@
package content
import (
"errors"
"io/fs"
"net/http"
"path/filepath"
)
func NewFilepath(getPath func() string) Handler {
return newFS(&filepathFS{
getPath: getPath,
})
}
var _ fsContent = &filepathFS{}
type filepathFS struct {
getPath func() string
}
func (f *filepathFS) ToFileServer(basePaths ...string) http.Handler {
root := f.getPath()
if root == "" {
return http.NotFoundHandler()
}
path := filepath.Join(append([]string{string(root)}, basePaths...)...)
return http.FileServer(http.Dir(path))
}
func (f *filepathFS) Open(name string) (fs.File, error) {
root := f.getPath()
if root == "" {
return nil, errors.New("filepath fs is not ready")
}
return http.Dir(root).Open(name)
}
func (f *filepathFS) Refresh() error {
return nil
}

7
internal/ui/dev.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build !embed
package ui
import "embed"
var staticContent embed.FS

12
internal/ui/embed.go Normal file
View File

@@ -0,0 +1,12 @@
//go:build embed
package ui
import (
"embed"
)
// content holds our static web server content.
//
//go:embed all:ui/*
var staticContent embed.FS

138
internal/ui/handler.go Normal file
View File

@@ -0,0 +1,138 @@
package ui
import (
"net/http"
"github.com/cnrancher/kube-explorer/internal/ui/content"
"github.com/rancher/apiserver/pkg/middleware"
"github.com/sirupsen/logrus"
)
type StringSetting func() string
type BoolSetting func() bool
func StaticSetting[T any](input T) func() T {
return func() T {
return input
}
}
type Handler struct {
contentHandlers map[string]content.Handler
pathSetting func() string
indexSetting func() string
releaseSetting func() bool
offlineSetting func() string
middleware func(http.Handler) http.Handler
indexMiddleware func(http.Handler) http.Handler
}
type Options struct {
// The location on disk of the UI files
Path StringSetting
// The HTTP URL of the index file to download
Index StringSetting
// Whether or not to run the UI offline, should return true/false/dynamic/embed
Offline StringSetting
// Whether or not is it release, if true UI will run offline if set to dynamic
ReleaseSetting BoolSetting
}
func NewUIHandler(opts *Options) *Handler {
if opts == nil {
opts = &Options{}
}
h := &Handler{
contentHandlers: make(map[string]content.Handler),
indexSetting: opts.Index,
offlineSetting: opts.Offline,
pathSetting: opts.Path,
releaseSetting: opts.ReleaseSetting,
middleware: middleware.Chain{
middleware.Gzip,
middleware.FrameOptions,
middleware.CacheMiddleware("json", "js", "css"),
}.Handler,
indexMiddleware: middleware.Chain{
middleware.Gzip,
middleware.NoCache,
middleware.FrameOptions,
middleware.ContentType,
}.Handler,
}
if h.indexSetting == nil {
h.indexSetting = StaticSetting("")
}
if h.offlineSetting == nil {
h.offlineSetting = StaticSetting("dynamic")
}
if h.pathSetting == nil {
h.pathSetting = StaticSetting("")
}
if h.releaseSetting == nil {
h.releaseSetting = StaticSetting(false)
}
h.contentHandlers["embed"] = content.NewEmbedded(staticContent, "ui")
h.contentHandlers["false"] = content.NewExternal(h.indexSetting)
h.contentHandlers["true"] = content.NewFilepath(h.pathSetting)
return h
}
func (h *Handler) content() content.Handler {
offline := h.offlineSetting()
if handler, ok := h.contentHandlers[offline]; ok {
return handler
}
embedHandler := h.contentHandlers["embed"]
filepathHandler := h.contentHandlers["true"]
externalHandler := h.contentHandlers["false"]
// default to dynamic
switch {
case h.pathSetting() != "":
if _, err := filepathHandler.GetIndex(); err == nil {
return filepathHandler
}
fallthrough
case h.releaseSetting():
// release must use embed first
return embedHandler
default:
// try embed
if _, err := embedHandler.GetIndex(); err == nil {
return embedHandler
}
return externalHandler
}
}
func (h *Handler) ServeAssets(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.content().ServeAssets(h.middleware, next).ServeHTTP(w, r)
})
}
func (h *Handler) ServeFaviconDashboard() http.Handler {
return h.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.content().ServeFaviconDashboard().ServeHTTP(w, r)
}))
}
func (h *Handler) IndexFile() http.Handler {
return h.indexMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rtn, err := h.content().GetIndex()
if err != nil {
logrus.Warnf("failed to serve index with error %v", err)
http.NotFoundHandler().ServeHTTP(w, r)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(rtn)
}))
}

31
internal/ui/routers.go Normal file
View File

@@ -0,0 +1,31 @@
package ui
import (
"net/http"
"strings"
"github.com/gorilla/mux"
)
func New(opt *Options) (http.Handler, APIUI) {
vue := NewUIHandler(opt)
router := mux.NewRouter()
router.UseEncodedPath()
router.Handle("/", http.RedirectHandler("/dashboard/", http.StatusFound))
router.Handle("/dashboard", http.RedirectHandler("/dashboard/", http.StatusFound))
router.Handle("/dashboard/", vue.IndexFile())
router.Handle("/favicon.png", vue.ServeFaviconDashboard())
router.Handle("/favicon.ico", vue.ServeFaviconDashboard())
router.PathPrefix("/dashboard/").Handler(vue.ServeAssets(vue.IndexFile()))
router.PathPrefix("/api-ui/").Handler(vue.ServeAssets(http.NotFoundHandler()))
router.PathPrefix("/k8s/clusters/local").HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
url := strings.TrimPrefix(req.URL.Path, "/k8s/clusters/local")
if url == "" {
url = "/"
}
http.Redirect(rw, req, url, http.StatusFound)
})
return router, apiUI(opt)
}

View File

@@ -0,0 +1,23 @@
package version
import (
"fmt"
"regexp"
"strings"
)
var (
Version = "dev"
GitCommit = "HEAD"
// K-EXPLORER
releasePattern = regexp.MustCompile("^v[0-9]")
)
func FriendlyVersion() string {
return fmt.Sprintf("%s (%s)", Version, GitCommit)
}
func IsRelease() bool {
return !strings.Contains(Version, "dev") && releasePattern.MatchString(Version)
}

50
main.go Normal file
View File

@@ -0,0 +1,50 @@
package main
import (
"os"
"github.com/rancher/steve/pkg/debug"
stevecli "github.com/rancher/steve/pkg/server/cli"
"github.com/rancher/steve/pkg/version"
"github.com/rancher/wrangler/v3/pkg/signals"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
keconfig "github.com/cnrancher/kube-explorer/internal/config"
"github.com/cnrancher/kube-explorer/internal/server"
)
func main() {
app := cli.NewApp()
app.Name = "kube-explorer"
app.Version = version.FriendlyVersion()
app.Usage = ""
app.Flags = joinFlags(
stevecli.Flags(&keconfig.Steve),
debug.Flags(&keconfig.Debug),
keconfig.Flags(),
)
app.Action = run
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
func run(_ *cli.Context) error {
ctx := signals.SetupSignalContext()
keconfig.Debug.MustSetupDebug()
s, err := server.ToServer(ctx, &keconfig.Steve, false)
if err != nil {
return err
}
return s.ListenAndServe(ctx, keconfig.Steve.HTTPSListenPort, keconfig.Steve.HTTPListenPort, nil)
}
func joinFlags(flags ...[]cli.Flag) []cli.Flag {
var rtn []cli.Flag
for _, flag := range flags {
rtn = append(rtn, flag...)
}
return rtn
}

View File

@@ -1,12 +0,0 @@
image: cnrancher/kube-explorer:latest
manifests:
-
image: cnrancher/kube-explorer:head-linux-amd64
platform:
architecture: amd64
os: linux
-
image: cnrancher/kube-explorer:head-linux-arm64
platform:
architecture: arm64
os: linux

View File

@@ -1,12 +0,0 @@
image: cnrancher/kube-explorer:{{build.tag}}
manifests:
-
image: cnrancher/kube-explorer:{{build.tag}}-linux-amd64
platform:
architecture: amd64
os: linux
-
image: cnrancher/kube-explorer:{{build.tag}}-linux-arm64
platform:
architecture: arm64
os: linux

View File

@@ -1,6 +1,7 @@
FROM alpine:3.13
COPY kube-explorer entrypoint.sh /usr/bin/
# Hack to make golang do files,dns search order
ENV LOCALDOMAIN=""
FROM registry.suse.com/bci/bci-minimal:15.6
ARG TARGETARCH
ARG TARGETOS
ENV ARCH=${TARGETARCH:-"amd64"} OS=${TARGETOS:-"linux"}
COPY entrypoint.sh /usr/bin/
COPY kube-explorer-${OS}-${ARCH} /usr/bin/kube-explorer
ENTRYPOINT ["entrypoint.sh"]

View File

@@ -1,54 +1,82 @@
#!/bin/bash
set -e
source $(dirname $0)/version
source "$(dirname $0)/version"
cd "$(dirname $0)/.."
rm -rf ./bin/* ./dist/*
OS_ARCH_ARG_LINUX="amd64 arm arm64"
OS_ARCH_ARG_DARWIN="amd64 arm64"
OS_ARCH_ARG_WINDOWS="amd64"
LD_INJECT_VALUES="-X github.com/rancher/steve/pkg/version.Version=$VERSION
-X github.com/rancher/steve/pkg/version.GitCommit=$COMMIT"
LD_INJECT_VALUES="-X github.com/cnrancher/kube-explorer/internal/version.Version=$VERSION
-X github.com/cnrancher/kube-explorer/internal/version.GitCommit=$COMMIT
-X github.com/cnrancher/kube-explorer/internal/config.APIUIVersion=$CATTLE_API_UI_VERSION"
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
pushd $GIT_SOURCE
case "$CROSS" in
"push")
for ARCH in ${OS_ARCH_ARG_LINUX}; do
OUTPUT_BIN="bin/kube-explorer-linux-$ARCH"
echo "Building binary for linux/$ARCH..."
GOARCH=$ARCH GOOS=linux CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES $LINKFLAGS" \
-o ${OUTPUT_BIN}
done
;;
"tag")
for ARCH in ${OS_ARCH_ARG_LINUX}; do
OUTPUT_BIN="bin/kube-explorer-linux-$ARCH"
echo "Building binary for linux/$ARCH..."
GOARCH=$ARCH GOOS=linux CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES $LINKFLAGS" \
-o ${OUTPUT_BIN}
done
if [ -n "$CROSS" ]; then
for ARCH in ${OS_ARCH_ARG_LINUX}; do
OUTPUT_BIN="bin/kube-explorer-linux-$ARCH"
echo "Building binary for linux/$ARCH..."
GOARCH=$ARCH GOOS=linux CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES $LINKFLAGS" \
-o ${OUTPUT_BIN}
done
for ARCH in ${OS_ARCH_ARG_DARWIN}; do
OUTPUT_BIN="bin/kube-explorer-darwin-$ARCH"
echo "Building binary for darwin/$ARCH..."
GOARCH=$ARCH GOOS=darwin CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES" \
-o ${OUTPUT_BIN}
done
for ARCH in ${OS_ARCH_ARG_DARWIN}; do
OUTPUT_BIN="bin/kube-explorer-darwin-$ARCH"
echo "Building binary for darwin/$ARCH..."
GOARCH=$ARCH GOOS=darwin CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES" \
-o ${OUTPUT_BIN}
done
else
# only build one for current platform
CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES $LINKFLAGS" \
-o bin/kube-explorer
fi
for ARCH in ${OS_ARCH_ARG_WINDOWS}; do
OUTPUT_BIN="bin/kube-explorer-windows-$ARCH.exe"
echo "Building binary for windows/$ARCH..."
GOARCH=$ARCH GOOS=windows CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES" \
-o ${OUTPUT_BIN}
done
;;
*)
# only build one for current platform
CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES $LINKFLAGS" \
-o "bin/kube-explorer-$(uname | tr '[:upper:]' '[:lower:]')-${ARCH}"
;;
esac
for f in $(ls ./bin/); do
if [[ $f != *darwin-arm64 ]]; then
upx -o $DAPPER_SOURCE/bin/$f bin/$f || true
fi
if [ -f $DAPPER_SOURCE/bin/$f ]; then
echo "UPX done!"
mkdir -p "./bin"
mkdir -p "./dist"
for f in ./bin/*; do
filename=$(basename "$f")
if [[ $filename != *darwin* && -z "$SKIP_COMPRESS" ]]; then
if upx -o "./dist/$filename" "$f"; then
echo "UPX done for $filename!"
else
echo "UPX failed for $filename, copying original file."
cp "$f" "./dist/$filename"
fi
else
echo "Copy origin file as UPX failed!!!"
cp bin/$f $DAPPER_SOURCE/bin/$f
cp "$f" "./dist/$filename"
fi
done
popd

View File

@@ -6,13 +6,8 @@ cd $(dirname $0)
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
pushd $GIT_SOURCE
CGO_ENABLED=0 go build \
-ldflags \
"$LINKFLAGS" \
-o bin/kube-explorer
mv bin/kube-explorer $DAPPER_SOURCE/bin/
popd

View File

@@ -1,18 +1,22 @@
#!/bin/bash
mkdir -p $(dirname $GIT_SOURCE)
source $(dirname $0)/version
pushd $(dirname $GIT_SOURCE)
cd "$(dirname $0)/.." || exit 1;
git clone --depth=1 --branch ${GIT_BRANCH} https://github.com/niusmallnan/steve.git
cd steve
git reset --hard ${GIT_COMMIT}
if [[ "$(uname)" == "Darwin" ]]; then
TAR_CMD="gtar"
else
TAR_CMD="tar"
fi
mkdir -p pkg/ui/ui/dashboard
cd pkg/ui/ui/dashboard
curl -sL https://pandaria-dashboard-ui.s3.ap-southeast-2.amazonaws.com/release-2.6-cn/kube-explorer-ui/${CATTLE_DASHBOARD_UI_VERSION}.tar.gz | tar xvzf - --strip-components=2
rm -rf internal/ui/ui/*
mkdir -p internal/ui/ui/dashboard
cd internal/ui/ui/dashboard || exit 1;
curl -sL https://pandaria-dashboard-ui.s3.ap-southeast-2.amazonaws.com/release-2.8-cn/kube-explorer-ui/${CATTLE_DASHBOARD_UI_VERSION}.tar.gz | $TAR_CMD xvzf - --strip-components=2
cp index.html ../index.html
popd
$(dirname $0)/hack_fs $GIT_SOURCE/pkg/ui/ui/
mkdir ../api-ui
cd ../api-ui || exit 1;
curl -sL https://releases.rancher.com/api-ui/${CATTLE_API_UI_VERSION}.tar.gz | $TAR_CMD xvzf - --strip-components=1

View File

@@ -2,10 +2,11 @@
set -e
mkdir -p bin dist
if [ -e ./scripts/$1 ]; then
if [ -e "./scripts/$1" ]; then
./scripts/"$@"
else
exec "$@"
fi
chown -R $DAPPER_UID:$DAPPER_GID .
chown -R "$DAPPER_UID:$DAPPER_GID" .

9
scripts/github_ci Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
set -e
cd $(dirname $0)
./download
./validate
./build

View File

@@ -1,42 +0,0 @@
#!/bin/bash
set -ex
#
# find . -type f -name "_*"
#
function hack_files() {
for f in $(find $1 -type f -name "_*"); do
name=$(basename $f)
updir=$(dirname $f)
new_path=$updir/${name:1}
echo "move $f $new_path"
mv $f $new_path
done
}
#
# find . -type d -name "_*"
#
function hack_dirs() {
for d in $(find $1 -mindepth 1 -maxdepth 1 -type d); do
if [[ ! -d $d ]]; then
continue
fi
name=$(basename $d)
if [[ ${name:0:1} == "_" ]]; then
updir=$(dirname $d)
new_path=$updir/${name:1}
echo "move $d $new_path"
mv $d $new_path
hack_dirs $new_path
continue
fi
hack_dirs $d
done
}
pushd $1
hack_files .
hack_dirs .
popd

View File

@@ -2,17 +2,8 @@
set -e
source $(dirname $0)/version
cd "$(dirname $0)/.."
pushd $DAPPER_SOURCE
cp dist/* package/
docker build -f package/Dockerfile -t "cnrancher/kube-explorer:$VERSION" package
if [ -f bin/kube-explorer-linux-${ARCH} ]; then
# For cross mode
cp bin/kube-explorer-linux-${ARCH} package/kube-explorer
else
# For common mode
cp bin/kube-explorer package/
fi
cd package
docker build -f Dockerfile -t cnrancher/kube-explorer:$VERSION .
popd

42
scripts/release-note Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env sh
set -e
source "$(dirname $0)/version"
cd "$(dirname $0)/.."
mkdir -p dist
TARGET_PATH="dist/release-note"
if [ -z "$(command -v release-notary)" ]; then
echo "release-notary is not found, skip generating release notes."
exit 0
fi
if [ -z "${GIT_TAG}" ]; then
echo "running this scrpit without tag, skip generating release notes."
exit 0
fi
GIT_TAG=$(echo "${GIT_TAG}" | grep -E "^v([0-9]+)\.([0-9]+)(\.[0-9]+)?(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$") || true
if [ "${GIT_TAG}" = "" ]; then
echo "git GIT_TAG is not validated, skip generating release notes."
exit 0
fi
for tag in $(git tag -l --sort=-v:refname); do
if [ "${tag}" = "${GIT_TAG}" ]; then
continue
fi
filterred=$(echo "${tag}" | grep -E "^v([0-9]+)\.([0-9]+)(\.[0-9]+)?(-rc[0-9]*)$") || true
if [ "${filterred}" = "" ]; then
echo "get real release tag ${tag}, stopping untag"
break
fi
git tag -d ${tag}
done
echo "following release notes will be published..."
release-notary publish -d 2>/dev/null | sed '1d' | sed '$d' > $TARGET_PATH
cat "$TARGET_PATH"

View File

@@ -1,7 +1,8 @@
#!/bin/bash
set -e
source $(dirname $0)/version
pushd $GIT_SOURCE
cd "$(dirname $0)/.."
if ! command -v golangci-lint; then
echo Running: go fmt
@@ -9,13 +10,11 @@ if ! command -v golangci-lint; then
exit
fi
#echo Running: golangci-lint
#golangci-lint run
echo Running: golangci-lint
golangci-lint run
echo Tidying up modules
go mod tidy
echo Verifying modules
go mod verify
popd