Compare commits

...

53 Commits

Author SHA1 Message Date
Yuxing Deng
5f51b892a9 fix: the usage is not compatible with old version 2024-11-25 09:43:22 +08:00
Yuxing Deng
a434be5b81 fix: wrong branch name 2024-10-21 21:52:17 +08:00
Yuxing Deng
a7787ce013 fix: add release-v branch to push workflow 2024-10-21 21:52:17 +08:00
Yuxing Deng
b8f1ad6f9e fix: CROSS push 2024-10-21 21:45:17 +08:00
Yuxing Deng
1633838017 fix: aliyun-metadata should have registry name for image 2024-10-21 21:14:13 +08:00
Yuxing Deng
4d17a53d3e feat: use goreleaser to build binaries 2024-10-21 16:29:04 +08:00
Yuxing Deng
4c1db385fc fix: Should handle stale socket file
Added a lock file to hold make sure that it's safe to rebuild socket file
2024-10-10 21:21:35 +08:00
Yuxing Deng
8d1433b07d feat: Support to serve http via socket 2024-10-09 22:53:08 +08:00
Yuxing Deng
db2728f0ed fix(docs): enhance Readme 2024-10-09 09:12:48 +08:00
Yuxing Deng
4f18ac4ae8 feat(ui): bump ui version to v2.9.2 2024-09-27 17:37:50 +08:00
Yuxing Deng
faee269cc1 feat(deps): bump golang & steve version 2024-09-27 17:37:50 +08:00
Yuxing Deng
9ce631d30f fix: use local version package 2024-09-26 10:29:40 +08:00
Yuxing Deng
2d512c0a72 fix: version is not parsed into build info 2024-09-26 09:49:16 +08:00
Yuxing Deng
5c987cd193 fix: Bump steve to fix the crd issue 2024-08-07 09:08:22 +08:00
Yuxing Deng
85925bbac7 fix: no need to new workflow file for release branch 2024-07-30 10:34:57 +08:00
Yuxing Deng
f435a24814 fix: Rename release branch ci name 2024-07-30 10:32:06 +08:00
Yuxing Deng
fb1f38e1ef feat: Add release branch ci 2024-07-30 10:31:00 +08:00
Yuxing Deng
67923822f5 fix: Container doesn't stop with signal 2024-07-29 17:24:10 +08:00
Yuxing Deng
1540341550 docs: Add docs for path prefix deploy example 2024-07-29 15:07:34 +08:00
Yuxing Deng
a5e53f2b17 feat: Support for nginx ingress path prefix
And validate should failed if git tree is dirty
2024-07-29 15:07:34 +08:00
Yuxing Deng
8f069c3b38 feat: Full support for proxy deploy
The index page will be handled correctly if the `XFF` and `X-API-URL-PREFIX` are set properly.
2024-07-25 17:32:01 +08:00
Yuxing Deng
568eda3e52 fix: Index content-type missing problem 2024-07-25 17:32:01 +08:00
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
61 changed files with 4247 additions and 503 deletions

3
.dockerignore Normal file
View File

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

View File

@@ -1,309 +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: image-scan-head
image: aquasec/trivy
commands:
- trivy image --no-progress --ignore-unfixed --severity HIGH,CRITICAL --security-checks vuln --exit-code 1 cnrancher/kube-explorer:head-linux-amd64
volumes:
- name: docker
path: /var/run/docker.sock
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

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

@@ -0,0 +1,19 @@
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
run: make ci

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

@@ -0,0 +1,92 @@
name: Push to Master
on:
push:
branches:
- release/v*
- main
- "release/v*"
tags:
- "v*.*.*" # Matches any tag that starts with 'v' and follows semantic versioning
env:
ALIYUN_REGISTRY: ${{ vars.ALIYUN_REGISTRY || '' }}
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: CI
env:
SKIP_PACKAGE: "true"
run: make ci
- name: Prepare for packaging image
run: cp dist/* package/
# aliyun image to test the docker build is ok
- name: Login to Aliyun ACR
if: ${{ vars.ALIYUN_REGISTRY != '' }}
uses: docker/login-action@v3
with:
registry: ${{ env.ALIYUN_REGISTRY }}
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_TOKEN }}
- name: Aliyun image docker meta
if: ${{ vars.ALIYUN_REGISTRY != '' }}
id: aliyun-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.ALIYUN_REGISTRY }}/${{ vars.REPO || 'cnrancher' }}/${{ vars.IMAGE || 'kube-explorer' }}
tags: |
type=ref,event=tag
type=ref,event=branch,suffix=-head
- name: Build to Aliyun
if: ${{ vars.ALIYUN_REGISTRY != '' }}
uses: docker/build-push-action@v6
with:
labels: ${{ steps.aliyun-meta.outputs.labels }}
tags: "${{ steps.aliyun-meta.outputs.tags }}"
context: package
push: true
# docker hub multi-arch image
- name: Login to Dockerhub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ vars.REPO || 'cnrancher' }}/${{ vars.IMAGE || 'kube-explorer' }}
tags: |
type=ref,event=tag
type=ref,event=branch,suffix=-head
- 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
labels: ${{ steps.meta.outputs.labels }}
tags: "${{ steps.meta.outputs.tags }}"
context: package
push: true
- name: Make release note
if: startsWith(github.ref, 'refs/tags/')
run: |
make release-note
- 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

8
.gitignore vendored
View File

@@ -19,3 +19,11 @@
/dist /dist
/build /build
*.swp *.swp
/.vscode
/vendor
/internal/ui/ui/
**/Dockerfile.dapper*
!**/Dockerfile.dapper
dist/

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"
}
]
}
}

75
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,75 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
dist: bin
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
builds:
- id: prod
env:
- CGO_ENABLED=0
targets:
- "darwin_amd64"
- "darwin_arm64"
- "linux_amd64"
- "linux_arm64"
- "linux_arm"
- "windows_amd64"
flags:
- -tags=embed
binary: '{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}'
ldflags: |
{{ if ne .Os "darwin" }}
-extldflags -static -s
{{ else }}
-s -w
{{ end }}
-X github.com/cnrancher/kube-explorer/internal/version.Version={{ .Env.VERSION }}
-X github.com/cnrancher/kube-explorer/internal/version.GitCommit={{ .Env.COMMIT }}
-X github.com/cnrancher/kube-explorer/internal/config.APIUIVersion={{ .Env.CATTLE_API_UI_VERSION }}
no_unique_dist_dir: true
- id: dev
env:
- CGO_ENABLED=0
targets:
- "linux_amd64"
- "linux_arm64"
flags:
- -tags=embed
binary: '{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}'
ldflags: |
{{ if ne .Os "darwin" }}
-extldflags -static -s
{{ else }}
-s -w
{{ end }}
-X github.com/cnrancher/kube-explorer/internal/version.Version={{ .Env.VERSION }}
-X github.com/cnrancher/kube-explorer/internal/version.GitCommit={{ .Env.COMMIT }}
-X github.com/cnrancher/kube-explorer/internal/config.APIUIVersion={{ .Env.CATTLE_API_UI_VERSION }}
no_unique_dist_dir: true
upx:
- compress: "5"
ids:
- prod
enabled: true
goos:
- linux
- windows
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"

View File

@@ -1,31 +1,30 @@
FROM golang:1.19 FROM goreleaser/goreleaser:v2.3.2 as goreleaser
FROM aevea/release-notary:0.9.2 as tools
FROM registry.suse.com/bci/golang:1.23
ARG PROXY
ARG GOPROXY
ARG DAPPER_HOST_ARCH ARG DAPPER_HOST_ARCH
ENV HOST_ARCH=${DAPPER_HOST_ARCH} ARCH=${DAPPER_HOST_ARCH} ENV HOST_ARCH=${DAPPER_HOST_ARCH} ARCH=${DAPPER_HOST_ARCH}
ENV https_proxy=${PROXY} \
http_proxy=${PROXY}
RUN apt-get update && \ RUN zypper -n install ca-certificates git-core wget curl unzip tar vim less file xz cosign docker
apt-get install -y ca-certificates git wget curl xz-utils && \
rm -f /bin/sh && ln -s /bin/bash /bin/sh && \ ENV UPX_VERSION 4.2.1
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 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/ mv /tmp/upx /usr/bin/
RUN if [ "${ARCH}" == "amd64" ]; then \ RUN if [ "${ARCH}" == "amd64" ]; then \
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.49.0; \ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.61.0; \
fi fi
COPY --from=goreleaser /usr/bin/goreleaser /usr/bin/goreleaser
COPY --from=tools /app/release-notary /usr/local/bin/
ENV CATTLE_DASHBOARD_UI_VERSION="v2.9.2-kube-explorer-ui-rc1"
ENV CATTLE_API_UI_VERSION="1.1.11"
ENV DOCKER_URL_amd64=https://get.docker.com/builds/Linux/x86_64/docker-1.10.3 \ ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS GOPROXY GITHUB_TOKEN GITHUB_REF GITHUB_REF_NAME BUILD_TARGET SKIP_PACKAGE
DOCKER_URL_arm=https://github.com/rancher/docker/releases/download/v1.10.3-ros1/docker-1.10.3_arm \ ENV DAPPER_SOURCE /go/src/github.com/cnrancher/kube-explorer
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="f8261f5f6e3a58f2e9a4b97ed5dc0a5f92b03c9e" \
GIT_BRANCH="ke/v0.2" \
GIT_SOURCE=${GOPATH}/src/github.com/rancher/steve \
CATTLE_DASHBOARD_UI_VERSION="v2.6.9-kube-explorer-ui-rc2"
ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS
ENV DAPPER_SOURCE /opt/kube-explorer
ENV DAPPER_OUTPUT ./bin ./dist ENV DAPPER_OUTPUT ./bin ./dist
ENV DAPPER_DOCKER_SOCKET true ENV DAPPER_DOCKER_SOCKET true
ENV DAPPER_RUN_ARGS "-v ke-pkg:/go/pkg -v ke-cache:/root/.cache/go-build --privileged" ENV DAPPER_RUN_ARGS "-v ke-pkg:/go/pkg -v ke-cache:/root/.cache/go-build --privileged"

View File

@@ -10,7 +10,7 @@ Please download the binary from the [release page](https://github.com/cnrancher/
To run an HTTP only server: To run an HTTP only server:
``` ```bash
./kube-explorer --kubeconfig=xxxx --http-listen-port=9898 --https-listen-port=0 ./kube-explorer --kubeconfig=xxxx --http-listen-port=9898 --https-listen-port=0
``` ```
@@ -22,20 +22,47 @@ Then, open the browser to visit http://x.x.x.x:9898 .
To debug on an AMD64 Linux host: To debug on an AMD64 Linux host:
``` ```bash
make dev make dev
# $basedir=/opt/ui/dist/ # $basedir=/opt/ui/dist/
# prepare the file trees like this # prepare the file trees like this
# $basedir/dashboard/ # $basedir/dashboard/
# $basedir/api-ui/
# $basedir/index.html # $basedir/index.html
# good to go! # good to go!
./kube-explorer --debug --ui-path /opt/ui/dist/ --http-listen-port=9898 --https-listen-port=0 ./bin/kube-explorer --debug --ui-path /opt/ui/dist/ --http-listen-port=9898 --https-listen-port=0
``` ```
To build all cross-platform binaries: To build all cross-platform binaries:
```bash
CROSS=tag make
``` ```
CROSS=1 make
``` ## Supported features
- Specified system default registry for shell image, e.g. `--system-default-registry`
- Specified shell image name, e.g. `--pod-image`
- Deployed behind proxy
- [Behind ingress with dns name](./deploy/kubectl/README.md)
- [Behind ingress with dns name and path prefix](./deploy/kubectl/path-prefix/Readme.md)
- Base auth via ingress such as [nginx](./deploy/kubectl/nginx-auth/README.md), [traefik-v1](./deploy/kubectl/traefik-v1-auth/README.md) and [traefik-v2](./deploy/kubectl/traefik-v2-auth/README.md)
## Support Matrix
Currently, there are several major versions under maintenance, each tailored to different Kubernetes version ranges due to the use of varying steve and client-go versions.
| Major | Target Rancher Branch | K8s version range |
| ----- | --------------------- | ----------------- |
| v0.4 | v2.8.x | >= 1.25 <= 1.28 |
| v0.5 | v2.9.x | >= 1.27 <= 1.30 |
Please use the proper kube-explorer version for your k8s setup.
## Related Projects
- kube-explorer ui([https://github.com/cnrancher/kube-explorer-ui](https://github.com/cnrancher/kube-explorer-ui))
- autok3s([https://github.com/cnrancher/autok3s](https://github.com/cnrancher/autok3s))
- api-ui([https://github.com/rancher/api-ui](https://github.com/rancher/api-ui))

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

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

View File

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

View File

@@ -0,0 +1,34 @@
# Deploy kube-explorer behind proxy with path prefix
> Supported since v0.5.0
The kube-explorer dashboard can be exposed behind a proxy and path prefix like `http://your-domain.com/kube-explorer`.
The deployment examples in this folder are:
- `nginx ingress`
- `traefik ingress`
## Serve with ingress
When serving with nginx/traefik ingress controller, the template ingress file needs to be modified. In the `*.tpl` file, you can spot the missing hostname like:
```yaml
spec:
rules:
- host: "${MY_IP}.sslip.io" # Replace with your actual domain
```
Replace your ip to `${MY_UP}`, this will use the [sslip.io](https://sslip.io/) dns service to resolve the hostname to the ingress ip address.
For the traefik ingress, it is using `v2` version of the traefik ingress schema which use middlewares to modify the proxy request. Both `stripPrefix` and `headers` are used.
For the nginx ingress, the annotations `nginx.ingress.kubernetes.io/x-forwarded-prefix` and `nginx.ingress.kubernetes.io/rewrite-target` are used to strip prefix and to add proxy request header.
## Serve with self-hosted proxy
If serving the kube-explorer with self-hosted proxy, following modifications are required when proxying:
- Rewrite the proxy request to strip the path prefix like `rewrite "(?i)/kube-explorer(/|$)(.*)" /$2 break;` in nginx configuration.
- Add header `X-API-URL-Prefix` or `X-Forwarded-Prefix` with the path prefix when proxying request like `proxy_set_header X-Forwarded-Prefix "/kube-explorer";` in nginx configuration.
Then kube-explorer will response the index.html with modified content with path prefix to the browser.

View File

@@ -0,0 +1,24 @@
# Note: please replace the host first
# To use sslip.io: https://sslip.io/
# To get your public IP: curl ipinfo.io/ip
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/x-forwarded-prefix: "/kube-explorer"
nginx.ingress.kubernetes.io/rewrite-target: /$2
name: kube-explorer-ingress
namespace: kube-system
spec:
rules:
- host: "${MY_IP}.sslip.io" # Replace with your actual domain
http:
paths:
- backend:
service:
name: kube-explorer
port:
name: http
path: /kube-explorer(/|$)(.*)
pathType: ImplementationSpecific

View File

@@ -0,0 +1,42 @@
# Note: please replace the host first
# To use sslip.io: https://sslip.io/
# To get your public IP: curl ipinfo.io/ip
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kube-explorer-ingress
namespace: kube-system
annotations:
traefik.ingress.kubernetes.io/router.middlewares: kube-system-prefix@kubernetescrd,kube-system-add-header@kubernetescrd
spec:
rules:
- host: "${MY_IP}.sslip.io" # Replace with your actual domain
http:
paths:
- path: /kube-explorer
pathType: Prefix
backend:
service:
name: kube-explorer
port:
name: http
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: prefix
namespace: kube-system
spec:
stripPrefix:
prefixes:
- /kube-explorer
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: add-header
namespace: kube-system
spec:
headers:
customRequestHeaders:
X-Forwarded-Prefix: "/kube-explorer" # Adds

View File

@@ -13,9 +13,9 @@ htpasswd -nb username password | base64
To install this mode, just run this script: To install this mode, just run this script:
``` ```
kubectl apply -f ./secret.yaml kubectl create -f ./secret.yaml
export MY_XIP_IO=$(curl -sL ipinfo.io/ip) export MY_IP=$(curl -sL ipinfo.io/ip)
envsubst < ./ingress.yaml.tpl | kubectl apply -f - envsubst < ./ingress.yaml.tpl | kubectl create -f -
``` ```
For more infos: https://doc.traefik.io/traefik/v1.7/configuration/backends/kubernetes/ 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 # 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 # To get your public IP: curl ipinfo.io/ip
apiVersion: networking.k8s.io/v1beta1 apiVersion: networking.k8s.io/v1beta1
@@ -16,7 +16,7 @@ metadata:
ingress.kubernetes.io/auth-remove-header: "true" ingress.kubernetes.io/auth-remove-header: "true"
spec: spec:
rules: rules:
- host: "${MY_XIP_IO}.xip.io" - host: "${MY_IP}.sslip.io"
http: http:
paths: paths:
- path: / - 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

118
go.mod Normal file
View File

@@ -0,0 +1,118 @@
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/Microsoft/go-winio v0.6.2
github.com/gorilla/mux v1.8.1
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146
github.com/rancher/dynamiclistener v0.6.0-rc2
github.com/rancher/steve v0.0.0-20240911190153-79304d93b49b
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/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
)

2005
go.sum Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,40 @@
package config
import (
"github.com/urfave/cli"
)
var InsecureSkipTLSVerify bool
var SystemDefaultRegistry string
var APIUIVersion = "1.1.11"
var ShellPodImage string
var BindAddress 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,
},
cli.StringFlag{
Name: "bind-address",
Destination: &BindAddress,
Usage: `Bind address with url format. The supported schemes are unix, tcp and namedpipe, e.g. unix:///path/to/kube-explorer.sock or namedpipe:/\.\pipe\kube-explorer`,
},
}
}

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

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

@@ -0,0 +1,115 @@
package server
import (
"context"
"net/http"
"strings"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/apiserver/pkg/urlbuilder"
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 handleProxyHeader(
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)
})
}
func handleProxyHeader(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if value := req.Header.Get("X-Forwarded-Prefix"); value != "" {
req.Header.Set(urlbuilder.PrefixHeader, value)
}
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)
}

View File

@@ -0,0 +1,52 @@
package server
import (
"context"
"log"
"net"
"net/http"
"github.com/cnrancher/kube-explorer/internal/config"
dynamicserver "github.com/rancher/dynamiclistener/server"
"github.com/rancher/steve/pkg/server"
"github.com/sirupsen/logrus"
)
func Serve(ctx context.Context, server *server.Server) error {
listener, ipOrPath, err := ensureListener(ctx)
if err != nil {
return err
}
if listener != nil {
defer listener.Close()
return serveSocket(ctx, ipOrPath, listener, server)
}
return server.ListenAndServe(ctx, config.Steve.HTTPSListenPort, config.Steve.HTTPListenPort, &dynamicserver.ListenOpts{
BindHost: ipOrPath,
})
}
func serveSocket(ctx context.Context, socketPath string, listener net.Listener, handler http.Handler) error {
logger := logrus.StandardLogger()
errorLog := log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags)
socketServer := &http.Server{
Handler: handler,
ErrorLog: errorLog,
BaseContext: func(_ net.Listener) context.Context {
return ctx
},
}
go func() {
logrus.Infof("Listening on %s", socketPath)
err := socketServer.Serve(listener)
if err != http.ErrServerClosed && err != nil {
logrus.Fatalf("https server failed: %v", err)
}
}()
go func() {
<-ctx.Done()
_ = socketServer.Shutdown(context.Background())
}()
<-ctx.Done()
return ctx.Err()
}

View File

@@ -0,0 +1,99 @@
//go:build unix
// +build unix
package server
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/cnrancher/kube-explorer/internal/config"
"github.com/sirupsen/logrus"
)
var _ net.Listener = &closerListener{}
type closerListener struct {
listener net.Listener
lockFile *os.File
}
func (l *closerListener) Accept() (net.Conn, error) {
return l.listener.Accept()
}
func (l *closerListener) Close() error {
return errors.Join(
l.listener.Close(),
l.lockFile.Close(),
os.RemoveAll(l.lockFile.Name()),
)
}
func (l *closerListener) Addr() net.Addr {
return l.listener.Addr()
}
func ensureListener(ctx context.Context) (net.Listener, string, error) {
if config.BindAddress == "" {
return nil, "", nil
}
u, err := url.Parse(config.BindAddress)
if err != nil {
return nil, "", err
}
switch u.Scheme {
case "":
return nil, config.BindAddress, nil
case "tcp":
return nil, u.Host, nil
case "unix":
listener, err := createCloserListener(ctx, u.Path)
if err != nil {
return nil, "", err
}
return listener, u.Path, err
default:
return nil, "", fmt.Errorf("Unsupported scheme %s, only tcp and unix are supported in UNIX OS", u.Scheme)
}
}
func createCloserListener(ctx context.Context, socketPath string) (net.Listener, error) {
lockFilePath := getLockFileName(socketPath)
lockFile, err := os.OpenFile(lockFilePath, os.O_RDONLY|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
lockErr := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if lockErr != nil {
return nil, fmt.Errorf("Socket file %s is in use, exiting", socketPath)
}
if _, err := os.Stat(socketPath); err == nil {
logrus.Infof("Removing stale socket file %s", socketPath)
_ = os.Remove(socketPath)
}
var lc net.ListenConfig
listener, err := lc.Listen(ctx, "unix", socketPath)
if err != nil {
return nil, err
}
return &closerListener{
listener: listener,
lockFile: lockFile,
}, nil
}
func getLockFileName(socketPath string) string {
return strings.TrimSuffix(socketPath, filepath.Ext(socketPath)) + ".lock"
}

View File

@@ -0,0 +1,35 @@
//go:build windows
// +build windows
package server
import (
"context"
"fmt"
"net"
"net/url"
"github.com/Microsoft/go-winio"
"github.com/cnrancher/kube-explorer/internal/config"
)
func ensureListener(_ context.Context) (net.Listener, string, error) {
if config.BindAddress == "" {
return nil, "", nil
}
u, err := url.Parse(config.BindAddress)
if err != nil {
return nil, "", err
}
switch u.Scheme {
case "":
return nil, config.BindAddress, nil
case "tcp":
return nil, u.Host, nil
case "namedpipe":
listener, err := winio.ListenPipe(u.Path, nil)
return listener, u.Path, err
default:
return nil, "", fmt.Errorf("Unsupported scheme %s, only tcp and namedpipe are supported in windows", u.Scheme)
}
}

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

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

@@ -0,0 +1,137 @@
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.Write(rtn)
}))
}

116
internal/ui/proxy.go Normal file
View File

@@ -0,0 +1,116 @@
package ui
import (
"bufio"
"bytes"
"fmt"
"io"
"net"
"net/http"
"strconv"
"github.com/rancher/apiserver/pkg/urlbuilder"
"k8s.io/apimachinery/pkg/util/proxy"
)
type RoundTripFunc func(*http.Request) (*http.Response, error)
func (r RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
func proxyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
scheme := urlbuilder.GetScheme(r)
host := urlbuilder.GetHost(r, scheme)
pathPrepend := r.Header.Get(urlbuilder.PrefixHeader)
if scheme == r.URL.Scheme && host == r.URL.Host && pathPrepend == "" {
next.ServeHTTP(w, r)
return
}
proxyRoundtrip := proxy.Transport{
Scheme: scheme,
Host: host,
PathPrepend: pathPrepend,
RoundTripper: RoundTripFunc(func(r *http.Request) (*http.Response, error) {
rw := &dummyResponseWriter{
next: w,
header: make(http.Header),
}
next.ServeHTTP(rw, r)
return rw.getResponse(r), nil
}),
}
//proxyRoundtripper will write the response in RoundTrip func
resp, _ := proxyRoundtrip.RoundTrip(r)
responseToWriter(resp, w)
})
}
var _ http.ResponseWriter = &dummyResponseWriter{}
var _ http.Hijacker = &dummyResponseWriter{}
type dummyResponseWriter struct {
next http.ResponseWriter
header http.Header
body bytes.Buffer
statusCode int
}
// Hijack implements http.Hijacker.
func (drw *dummyResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := drw.next.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, fmt.Errorf("")
}
// Header implements the http.ResponseWriter interface.
func (drw *dummyResponseWriter) Header() http.Header {
return drw.header
}
// Write implements the http.ResponseWriter interface.
func (drw *dummyResponseWriter) Write(b []byte) (int, error) {
return drw.body.Write(b)
}
// WriteHeader implements the http.ResponseWriter interface.
func (drw *dummyResponseWriter) WriteHeader(statusCode int) {
drw.statusCode = statusCode
}
// GetStatusCode returns the status code written to the response.
func (drw *dummyResponseWriter) GetStatusCode() int {
if drw.statusCode == 0 {
return 200
}
return drw.statusCode
}
func (drw *dummyResponseWriter) getResponse(req *http.Request) *http.Response {
return &http.Response{
Status: strconv.Itoa(drw.GetStatusCode()),
StatusCode: drw.GetStatusCode(),
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Request: req,
Header: drw.header,
Body: io.NopCloser(&drw.body),
}
}
func responseToWriter(resp *http.Response, writer http.ResponseWriter) {
for k, v := range resp.Header {
writer.Header()[k] = v
}
if resp.StatusCode != http.StatusOK {
writer.WriteHeader(resp.StatusCode)
}
_, _ = io.Copy(writer, resp.Body)
}

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 proxyMiddleware(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/cnrancher/kube-explorer/internal/version"
"github.com/rancher/steve/pkg/debug"
stevecli "github.com/rancher/steve/pkg/server/cli"
"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 server.Serve(ctx, s)
}
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,4 +1,7 @@
FROM registry.suse.com/bci/bci-minimal:15.4 FROM registry.suse.com/bci/bci-base:15.6
ARG TARGETARCH
COPY kube-explorer entrypoint.sh /usr/bin/ ARG TARGETOS
ENTRYPOINT ["entrypoint.sh"] ENV ARCH=${TARGETARCH:-"amd64"} OS=${TARGETOS:-"linux"}
RUN zypper install -y catatonit
COPY kube-explorer-${OS}-${ARCH} /usr/bin/kube-explorer
ENTRYPOINT [ "/usr/bin/catatonit", "--", "kube-explorer" ]

View File

@@ -1,3 +0,0 @@
#!/bin/sh
kube-explorer "${@}"

View File

@@ -1,54 +1,38 @@
#!/bin/bash #!/bin/bash
set -e set -e
source $(dirname $0)/version source "$(dirname $0)/version"
cd "$(dirname $0)/.."
OS_ARCH_ARG_LINUX="amd64 arm arm64" rm -rf ./bin/* ./dist/*
OS_ARCH_ARG_DARWIN="amd64 arm64"
LD_INJECT_VALUES="-X github.com/rancher/steve/pkg/version.Version=$VERSION BUILD_TARGET="${BUILD_TARGET:-dev}"
-X github.com/rancher/steve/pkg/version.GitCommit=$COMMIT" CROSS=${CROSS:-}
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s" if [[ ${GITHUB_REF} == refs/tags/* ]]; then
CROSS=tag
pushd $GIT_SOURCE elif [ -n "${GITHUB_REF}" ]; then
CROSS=push
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
else
# only build one for current platform
CGO_ENABLED=0 go build -tags embed \
-ldflags \
"$LD_INJECT_VALUES $LINKFLAGS" \
-o bin/kube-explorer
fi fi
for f in $(ls ./bin/); do case "$CROSS" in
if [[ $f != *darwin-arm64 ]]; then "tag")
upx -o $DAPPER_SOURCE/bin/$f bin/$f || true BUILD_TARGET="prod"
fi ;;
if [ -f $DAPPER_SOURCE/bin/$f ]; then "push")
echo "UPX done!" ;;
else *)
echo "Copy origin file as UPX failed!!!" BUILD_ARG="${BUILD_ARG} --single-target"
cp bin/$f $DAPPER_SOURCE/bin/$f ;;
fi esac
done
popd BUILD_ARG="${BUILD_ARG:-} --skip validate --id ${BUILD_TARGET}"
mkdir -p "./bin"
# upx is handled by goreleaser
VERSION=${VERSION} COMMIT=${COMMIT} goreleaser build $BUILD_ARG
mkdir -p "./dist"
cp -r bin/kube-explorer-* dist/

View File

@@ -6,4 +6,6 @@ cd $(dirname $0)
./download ./download
./validate ./validate
./build ./build
./package if [ -z "${SKIP_PACKAGE}" ]; then
./package
fi

View File

@@ -1,18 +1,15 @@
#!/bin/bash #!/bin/bash
set -e set -e
cd $(dirname $0) cd "$(dirname $0)/.."
./download ./scripts/download
source $(dirname $0)/version
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s" [ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
pushd $GIT_SOURCE
CGO_ENABLED=0 go build \ CGO_ENABLED=0 go build \
-ldflags \ -ldflags \
"$LINKFLAGS" \ "$LINKFLAGS" \
-o bin/kube-explorer -o bin/kube-explorer
mv bin/kube-explorer $DAPPER_SOURCE/bin/
popd

View File

@@ -1,18 +1,22 @@
#!/bin/bash #!/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 if [[ "$(uname)" == "Darwin" ]]; then
cd steve TAR_CMD="gtar"
git reset --hard ${GIT_COMMIT} else
TAR_CMD="tar"
fi
mkdir -p pkg/ui/ui/dashboard rm -rf internal/ui/ui/*
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 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.9-cn/kube-explorer-ui/${CATTLE_DASHBOARD_UI_VERSION}.tar.gz | $TAR_CMD xvzf - --strip-components=2
cp index.html ../index.html cp index.html ../index.html
popd mkdir ../api-ui
cd ../api-ui || exit 1;
$(dirname $0)/hack_fs $GIT_SOURCE/pkg/ui/ui/ 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 set -e
mkdir -p bin dist mkdir -p bin dist
if [ -e ./scripts/$1 ]; then if [ -e "./scripts/$1" ]; then
./scripts/"$@" ./scripts/"$@"
else else
exec "$@" exec "$@"
fi fi
chown -R $DAPPER_UID:$DAPPER_GID .
chown -R "$DAPPER_UID:$DAPPER_GID" .

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 set -e
source $(dirname $0)/version 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 #!/bin/bash
set -e set -e
source $(dirname $0)/version
pushd $GIT_SOURCE cd "$(dirname $0)/.."
if ! command -v golangci-lint; then if ! command -v golangci-lint; then
echo Running: go fmt echo Running: go fmt
@@ -9,8 +10,8 @@ if ! command -v golangci-lint; then
exit exit
fi fi
#echo Running: golangci-lint echo Running: golangci-lint
#golangci-lint run golangci-lint run
echo Tidying up modules echo Tidying up modules
go mod tidy go mod tidy
@@ -18,4 +19,13 @@ go mod tidy
echo Verifying modules echo Verifying modules
go mod verify go mod verify
popd dirty_files="$(git status --porcelain --untracked-files=no)"
if [ -n "$dirty_files" ]; then
echo "Encountered dirty repo! Aborting."
echo "If you're seeing this, it means there are uncommitted changes in the repo."
echo "If you're seeing this in CI, it probably means that your Go modules aren't tidy, or more generally that running"
echo "validation would result in changes to the repo. Make sure you're up to date with the upstream branch and run"
echo "'go mod tidy' and commit the changes, if any. The offending changed files are as follows:"
echo "$dirty_files"
exit 1
fi

View File

@@ -1,11 +1,15 @@
#!/bin/bash #!/bin/bash
if [[ ${GITHUB_REF} == refs/tags/* ]]; then
GIT_TAG=${GIT_TAG:-${GITHUB_REF_NAME}}
fi
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
DIRTY="-dirty" DIRTY="-dirty"
fi fi
COMMIT=$(git rev-parse --short HEAD) COMMIT=$(git rev-parse --short HEAD)
GIT_TAG=${DRONE_TAG:-$(git tag -l --contains HEAD | head -n 1)} GIT_TAG=${GIT_TAG:-$(git tag -l --contains HEAD | head -n 1)}
if [[ -z "$DIRTY" && -n "$GIT_TAG" ]]; then if [[ -z "$DIRTY" && -n "$GIT_TAG" ]]; then
VERSION=$GIT_TAG VERSION=$GIT_TAG