mirror of
https://github.com/cnrancher/kube-explorer.git
synced 2025-09-02 06:55:01 +00:00
Compare commits
51 Commits
v0.3.0-rc1
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
5f51b892a9 | ||
|
a434be5b81 | ||
|
a7787ce013 | ||
|
b8f1ad6f9e | ||
|
1633838017 | ||
|
4d17a53d3e | ||
|
4c1db385fc | ||
|
8d1433b07d | ||
|
db2728f0ed | ||
|
4f18ac4ae8 | ||
|
faee269cc1 | ||
|
9ce631d30f | ||
|
2d512c0a72 | ||
|
5c987cd193 | ||
|
85925bbac7 | ||
|
f435a24814 | ||
|
fb1f38e1ef | ||
|
67923822f5 | ||
|
1540341550 | ||
|
a5e53f2b17 | ||
|
8f069c3b38 | ||
|
568eda3e52 | ||
|
eacc47482e | ||
|
004e4751c8 | ||
|
8bf22555dd | ||
|
896e03e279 | ||
|
979b4991fa | ||
|
aec9926ed8 | ||
|
2f3c1e6ab5 | ||
|
faa83722a0 | ||
|
cd955243b6 | ||
|
2b39db9f07 | ||
|
4dc1acb1f2 | ||
|
989d087b99 | ||
|
c214e6ba6a | ||
|
390b11caef | ||
|
e016261c4b | ||
|
c43288964a | ||
|
70e586976d | ||
|
d0ce0e28bf | ||
|
ad0a0c0cb3 | ||
|
651d499086 | ||
|
8e592b1a3c | ||
|
c1f5fda228 | ||
|
10e5323c95 | ||
|
ea49f9d3b4 | ||
|
b0b81ba87d | ||
|
e757347def | ||
|
f4970b85a2 | ||
|
bfae192748 | ||
|
3810cd702f |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
/bin
|
||||
/dist
|
||||
/internal/ui/ui
|
309
.drone.yml
309
.drone.yml
@@ -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
165
.drone_backup.yml
Normal 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
19
.github/workflows/pr.yaml
vendored
Normal 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
92
.github/workflows/push.yaml
vendored
Normal 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
8
.gitignore
vendored
@@ -19,3 +19,11 @@
|
||||
/dist
|
||||
/build
|
||||
*.swp
|
||||
|
||||
/.vscode
|
||||
/vendor
|
||||
/internal/ui/ui/
|
||||
**/Dockerfile.dapper*
|
||||
!**/Dockerfile.dapper
|
||||
|
||||
dist/
|
||||
|
68
.golangci.json
Normal file
68
.golangci.json
Normal 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
75
.goreleaser.yaml
Normal 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:"
|
@@ -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
|
||||
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/v4.0.1/upx-4.0.1-${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 cosign 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.49.0; \
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.61.0; \
|
||||
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 \
|
||||
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="4bcc9108508cd8112a28903f76bfca2fdd0bfc9d" \
|
||||
GIT_BRANCH="ke/v0.3" \
|
||||
GIT_SOURCE=${GOPATH}/src/github.com/rancher/steve \
|
||||
CATTLE_DASHBOARD_UI_VERSION="v2.7.0-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 GITHUB_TOKEN GITHUB_REF GITHUB_REF_NAME BUILD_TARGET SKIP_PACKAGE
|
||||
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"
|
||||
|
37
README.md
37
README.md
@@ -10,7 +10,7 @@ Please download the binary from the [release page](https://github.com/cnrancher/
|
||||
|
||||
To run an HTTP only server:
|
||||
|
||||
```
|
||||
```bash
|
||||
./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:
|
||||
|
||||
```
|
||||
```bash
|
||||
make dev
|
||||
|
||||
# $basedir=/opt/ui/dist/
|
||||
# prepare the file trees like this
|
||||
# $basedir/dashboard/
|
||||
# $basedir/api-ui/
|
||||
# $basedir/index.html
|
||||
|
||||
# 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:
|
||||
|
||||
```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
12
deploy/kubectl/README.md
Normal 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)
|
@@ -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/
|
||||
|
@@ -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
|
||||
|
34
deploy/kubectl/path-prefix/Readme.md
Normal file
34
deploy/kubectl/path-prefix/Readme.md
Normal 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.
|
24
deploy/kubectl/path-prefix/nginx-ingress.yaml.tpl
Normal file
24
deploy/kubectl/path-prefix/nginx-ingress.yaml.tpl
Normal 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
|
||||
|
42
deploy/kubectl/path-prefix/traefik-ingress.yaml.tpl
Normal file
42
deploy/kubectl/path-prefix/traefik-ingress.yaml.tpl
Normal 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
|
@@ -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/
|
@@ -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: /
|
21
deploy/kubectl/traefik-v2-auth/README.md
Normal file
21
deploy/kubectl/traefik-v2-auth/README.md
Normal 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/
|
25
deploy/kubectl/traefik-v2-auth/ingress.yaml.tpl
Normal file
25
deploy/kubectl/traefik-v2-auth/ingress.yaml.tpl
Normal 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
|
28
deploy/kubectl/traefik-v2-auth/middleware.yaml
Normal file
28
deploy/kubectl/traefik-v2-auth/middleware.yaml
Normal 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
118
go.mod
Normal 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
|
||||
)
|
40
internal/config/flags.go
Normal file
40
internal/config/flags.go
Normal 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
11
internal/config/steve.go
Normal 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
|
||||
)
|
80
internal/resources/cluster/cluster.go
Normal file
80
internal/resources/cluster/cluster.go
Normal 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"
|
||||
}
|
162
internal/resources/cluster/shell.go
Normal file
162
internal/resources/cluster/shell.go
Normal 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
115
internal/server/config.go
Normal 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)
|
||||
})
|
||||
}
|
16
internal/server/delete_option_store.go
Normal file
16
internal/server/delete_option_store.go
Normal 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)
|
||||
}
|
52
internal/server/listener.go
Normal file
52
internal/server/listener.go
Normal 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()
|
||||
}
|
99
internal/server/listener_unix.go
Normal file
99
internal/server/listener_unix.go
Normal 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"
|
||||
}
|
35
internal/server/listener_windows.go
Normal file
35
internal/server/listener_windows.go
Normal 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
55
internal/ui/apiui.go
Normal 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")
|
||||
}
|
24
internal/ui/content/content.go
Normal file
24
internal/ui/content/content.go
Normal 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()
|
||||
}
|
97
internal/ui/content/external.go
Normal file
97
internal/ui/content/external.go
Normal 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
71
internal/ui/content/fs.go
Normal 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)
|
||||
}
|
43
internal/ui/content/fs_embed.go
Normal file
43
internal/ui/content/fs_embed.go
Normal 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...))
|
||||
}
|
41
internal/ui/content/fs_filepath.go
Normal file
41
internal/ui/content/fs_filepath.go
Normal 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
7
internal/ui/dev.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !embed
|
||||
|
||||
package ui
|
||||
|
||||
import "embed"
|
||||
|
||||
var staticContent embed.FS
|
12
internal/ui/embed.go
Normal file
12
internal/ui/embed.go
Normal 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
137
internal/ui/handler.go
Normal 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
116
internal/ui/proxy.go
Normal 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
31
internal/ui/routers.go
Normal 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)
|
||||
}
|
23
internal/version/version.go
Normal file
23
internal/version/version.go
Normal 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
50
main.go
Normal 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
|
||||
}
|
@@ -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
|
@@ -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
|
@@ -1,4 +1,7 @@
|
||||
FROM registry.suse.com/bci/bci-minimal:15.4
|
||||
|
||||
COPY kube-explorer entrypoint.sh /usr/bin/
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
FROM registry.suse.com/bci/bci-base:15.6
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
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" ]
|
||||
|
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
kube-explorer "${@}"
|
@@ -1,54 +1,38 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
source $(dirname $0)/version
|
||||
source "$(dirname $0)/version"
|
||||
cd "$(dirname $0)/.."
|
||||
|
||||
OS_ARCH_ARG_LINUX="amd64 arm arm64"
|
||||
OS_ARCH_ARG_DARWIN="amd64 arm64"
|
||||
rm -rf ./bin/* ./dist/*
|
||||
|
||||
LD_INJECT_VALUES="-X github.com/rancher/steve/pkg/version.Version=$VERSION
|
||||
-X github.com/rancher/steve/pkg/version.GitCommit=$COMMIT"
|
||||
BUILD_TARGET="${BUILD_TARGET:-dev}"
|
||||
CROSS=${CROSS:-}
|
||||
|
||||
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
|
||||
|
||||
pushd $GIT_SOURCE
|
||||
|
||||
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
|
||||
if [[ ${GITHUB_REF} == refs/tags/* ]]; then
|
||||
CROSS=tag
|
||||
elif [ -n "${GITHUB_REF}" ]; then
|
||||
CROSS=push
|
||||
fi
|
||||
|
||||
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!"
|
||||
else
|
||||
echo "Copy origin file as UPX failed!!!"
|
||||
cp bin/$f $DAPPER_SOURCE/bin/$f
|
||||
fi
|
||||
done
|
||||
case "$CROSS" in
|
||||
"tag")
|
||||
BUILD_TARGET="prod"
|
||||
;;
|
||||
"push")
|
||||
;;
|
||||
*)
|
||||
BUILD_ARG="${BUILD_ARG} --single-target"
|
||||
;;
|
||||
esac
|
||||
|
||||
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/
|
||||
|
@@ -6,4 +6,6 @@ cd $(dirname $0)
|
||||
./download
|
||||
./validate
|
||||
./build
|
||||
./package
|
||||
if [ -z "${SKIP_PACKAGE}" ]; then
|
||||
./package
|
||||
fi
|
11
scripts/dev
11
scripts/dev
@@ -1,18 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd $(dirname $0)
|
||||
./download
|
||||
cd "$(dirname $0)/.."
|
||||
./scripts/download
|
||||
|
||||
source $(dirname $0)/version
|
||||
|
||||
[ "$(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
|
||||
|
@@ -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.7-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.9-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
|
||||
|
@@ -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" .
|
||||
|
@@ -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
|
@@ -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
42
scripts/release-note
Executable 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"
|
@@ -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,8 +10,8 @@ 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
|
||||
@@ -18,4 +19,13 @@ go mod tidy
|
||||
echo Verifying modules
|
||||
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
|
||||
|
@@ -1,11 +1,15 @@
|
||||
#!/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
|
||||
DIRTY="-dirty"
|
||||
fi
|
||||
|
||||
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
|
||||
VERSION=$GIT_TAG
|
||||
|
Reference in New Issue
Block a user