mirror of
https://github.com/cnrancher/kube-explorer.git
synced 2025-09-03 15:35:09 +00:00
Compare commits
23 Commits
v0.5.0-rc3
...
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 |
2
.github/workflows/pr.yaml
vendored
2
.github/workflows/pr.yaml
vendored
@@ -16,6 +16,4 @@ jobs:
|
|||||||
- name: Commitsar check
|
- name: Commitsar check
|
||||||
uses: aevea/commitsar@v0.20.2
|
uses: aevea/commitsar@v0.20.2
|
||||||
- name: Build to test
|
- name: Build to test
|
||||||
env:
|
|
||||||
SKIP_COMPRESS: "true"
|
|
||||||
run: make ci
|
run: make ci
|
||||||
|
93
.github/workflows/push.yaml
vendored
93
.github/workflows/push.yaml
vendored
@@ -3,9 +3,14 @@ name: Push to Master
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
- release/v*
|
||||||
- main
|
- main
|
||||||
|
- "release/v*"
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*' # Matches any tag that starts with 'v' and follows semantic versioning
|
- "v*.*.*" # Matches any tag that starts with 'v' and follows semantic versioning
|
||||||
|
|
||||||
|
env:
|
||||||
|
ALIYUN_REGISTRY: ${{ vars.ALIYUN_REGISTRY || '' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
@@ -15,67 +20,73 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Login to Aliyun ACR
|
||||||
|
if: ${{ vars.ALIYUN_REGISTRY != '' }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: registry.cn-shenzhen.aliyuncs.com
|
registry: ${{ env.ALIYUN_REGISTRY }}
|
||||||
username: ${{ secrets.ACR_USERNAME }}
|
username: ${{ secrets.ACR_USERNAME }}
|
||||||
password: ${{ secrets.ACR_TOKEN }}
|
password: ${{ secrets.ACR_TOKEN }}
|
||||||
if: ${{ vars.ALIYUN == 'true' }}
|
- 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
|
- name: Login to Dockerhub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: CI
|
- name: Docker meta
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
id: meta
|
||||||
env:
|
uses: docker/metadata-action@v5
|
||||||
CROSS: push
|
with:
|
||||||
run: make github_ci
|
images: ${{ vars.REPO || 'cnrancher' }}/${{ vars.IMAGE || 'kube-explorer' }}
|
||||||
|
tags: |
|
||||||
- name: CI
|
type=ref,event=tag
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
type=ref,event=branch,suffix=-head
|
||||||
env:
|
- name: Set up QEMU
|
||||||
CROSS: tag
|
|
||||||
run: |
|
|
||||||
make github_ci
|
|
||||||
make release-note
|
|
||||||
|
|
||||||
- name: Prepare for packaging image
|
|
||||||
run: cp dist/* package/
|
|
||||||
- name: Set docker iamge name
|
|
||||||
id: image-name
|
|
||||||
env:
|
|
||||||
REPO_OVERRIDE: ${{ vars.REPO || 'cnrancher' }}
|
|
||||||
IMAGE_OVERRIDE: ${{ vars.IMAGE || 'kube-explorer' }}
|
|
||||||
run: |
|
|
||||||
tag_name=latest;
|
|
||||||
if [[ ${GITHUB_REF} == refs/tags/* ]]; then tag_name=${GITHUB_REF#refs/tags/}; fi;
|
|
||||||
echo "image_name=${REPO_OVERRIDE}/${IMAGE_OVERRIDE}:${tag_name}" >> $GITHUB_OUTPUT;
|
|
||||||
-
|
|
||||||
name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
-
|
- name: Set up Docker Buildx
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Build to Dockerhub
|
- name: Build to Dockerhub
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
tags: "${{ steps.image-name.outputs.image_name }}"
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
tags: "${{ steps.meta.outputs.tags }}"
|
||||||
context: package
|
context: package
|
||||||
push: true
|
push: true
|
||||||
- name: Build to Aliyun
|
|
||||||
uses: docker/build-push-action@v6
|
- name: Make release note
|
||||||
with:
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
tags: registry.cn-shenzhen.aliyuncs.com/${{ steps.image-name.outputs.image_name }}
|
run: |
|
||||||
context: package
|
make release-note
|
||||||
push: true
|
|
||||||
if: ${{ vars.ALIYUN == 'true' }}
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
files: dist/kube-explorer-*
|
files: dist/kube-explorer-*
|
||||||
body_path: dist/release-note
|
body_path: dist/release-note
|
||||||
draft: true
|
draft: true
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -23,3 +23,7 @@
|
|||||||
/.vscode
|
/.vscode
|
||||||
/vendor
|
/vendor
|
||||||
/internal/ui/ui/
|
/internal/ui/ui/
|
||||||
|
**/Dockerfile.dapper*
|
||||||
|
!**/Dockerfile.dapper
|
||||||
|
|
||||||
|
dist/
|
||||||
|
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,6 +1,7 @@
|
|||||||
|
FROM goreleaser/goreleaser:v2.3.2 as goreleaser
|
||||||
FROM aevea/release-notary:0.9.2 as tools
|
FROM aevea/release-notary:0.9.2 as tools
|
||||||
|
|
||||||
FROM registry.suse.com/bci/golang:1.22
|
FROM registry.suse.com/bci/golang:1.23
|
||||||
ARG PROXY
|
ARG PROXY
|
||||||
ARG GOPROXY
|
ARG GOPROXY
|
||||||
ARG DAPPER_HOST_ARCH
|
ARG DAPPER_HOST_ARCH
|
||||||
@@ -8,20 +9,21 @@ ENV HOST_ARCH=${DAPPER_HOST_ARCH} ARCH=${DAPPER_HOST_ARCH}
|
|||||||
ENV https_proxy=${PROXY} \
|
ENV https_proxy=${PROXY} \
|
||||||
http_proxy=${PROXY}
|
http_proxy=${PROXY}
|
||||||
|
|
||||||
RUN zypper -n install ca-certificates git-core wget curl unzip tar vim less file xz
|
RUN zypper -n install ca-certificates git-core wget curl unzip tar vim less file xz cosign docker
|
||||||
RUN zypper install -y -f docker
|
|
||||||
|
|
||||||
ENV UPX_VERSION 4.2.1
|
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 && \
|
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.54.2; \
|
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/
|
COPY --from=tools /app/release-notary /usr/local/bin/
|
||||||
ENV CATTLE_DASHBOARD_UI_VERSION="v2.8.0-kube-explorer-ui-rc3"
|
ENV CATTLE_DASHBOARD_UI_VERSION="v2.9.2-kube-explorer-ui-rc1"
|
||||||
|
ENV CATTLE_API_UI_VERSION="1.1.11"
|
||||||
ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS GOPROXY SKIP_COMPRESS GITHUB_REPOSITORY GITHUB_TOKEN
|
|
||||||
|
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_SOURCE /go/src/github.com/cnrancher/kube-explorer
|
||||||
ENV DAPPER_OUTPUT ./bin ./dist
|
ENV DAPPER_OUTPUT ./bin ./dist
|
||||||
ENV DAPPER_DOCKER_SOCKET true
|
ENV DAPPER_DOCKER_SOCKET true
|
||||||
|
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:
|
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))
|
||||||
|
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
|
5
go.mod
5
go.mod
@@ -5,9 +5,11 @@ go 1.22.0
|
|||||||
replace k8s.io/client-go => k8s.io/client-go v0.30.1
|
replace k8s.io/client-go => k8s.io/client-go v0.30.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.6.2
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146
|
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146
|
||||||
github.com/rancher/steve v0.0.0-20240709130809-47871606146c
|
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/rancher/wrangler/v3 v3.0.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/urfave/cli v1.22.15
|
github.com/urfave/cli v1.22.15
|
||||||
@@ -62,7 +64,6 @@ require (
|
|||||||
github.com/prometheus/client_model v0.4.0 // indirect
|
github.com/prometheus/client_model v0.4.0 // indirect
|
||||||
github.com/prometheus/common v0.44.0 // indirect
|
github.com/prometheus/common v0.44.0 // indirect
|
||||||
github.com/prometheus/procfs v0.10.1 // indirect
|
github.com/prometheus/procfs v0.10.1 // indirect
|
||||||
github.com/rancher/dynamiclistener v0.6.0-rc2 // indirect
|
|
||||||
github.com/rancher/kubernetes-provider-detector v0.1.5 // indirect
|
github.com/rancher/kubernetes-provider-detector v0.1.5 // indirect
|
||||||
github.com/rancher/lasso v0.0.0-20240705194423-b2a060d103c1 // indirect
|
github.com/rancher/lasso v0.0.0-20240705194423-b2a060d103c1 // indirect
|
||||||
github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9 // indirect
|
github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9 // indirect
|
||||||
|
6
go.sum
6
go.sum
@@ -601,6 +601,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
@@ -1029,8 +1031,8 @@ github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9 h1:AlRMRs5mHJcdiK83
|
|||||||
github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9/go.mod h1:dyjfXBsNiroPWOdUZe7diUOUSLf6HQ/r2kEpwH/8zas=
|
github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9/go.mod h1:dyjfXBsNiroPWOdUZe7diUOUSLf6HQ/r2kEpwH/8zas=
|
||||||
github.com/rancher/remotedialer v0.3.2 h1:kstZbRwPS5gPWpGg8VjEHT2poHtArs+Fc317YM8JCzU=
|
github.com/rancher/remotedialer v0.3.2 h1:kstZbRwPS5gPWpGg8VjEHT2poHtArs+Fc317YM8JCzU=
|
||||||
github.com/rancher/remotedialer v0.3.2/go.mod h1:Ys004RpJuTLSm+k4aYUCoFiOOad37ubYev3TkOFg/5w=
|
github.com/rancher/remotedialer v0.3.2/go.mod h1:Ys004RpJuTLSm+k4aYUCoFiOOad37ubYev3TkOFg/5w=
|
||||||
github.com/rancher/steve v0.0.0-20240709130809-47871606146c h1:PIaN0/KUyGcqEcT6GyjUidld2lgGkGxS4dmC3Je3dFs=
|
github.com/rancher/steve v0.0.0-20240911190153-79304d93b49b h1:2DhkNKDgKPI2PcJRGacpJ9dX9SWgKuhwbz5GlpHS1No=
|
||||||
github.com/rancher/steve v0.0.0-20240709130809-47871606146c/go.mod h1:Za4nSt0V6kIHRfUo6jTXKkv6ABMMCHINA8EzhzygCfk=
|
github.com/rancher/steve v0.0.0-20240911190153-79304d93b49b/go.mod h1:cXF0TFURkjN98p/0nt7Y8F1IEtT1nZTSYrQu6HQqL14=
|
||||||
github.com/rancher/wrangler/v3 v3.0.0 h1:IHHCA+vrghJDPxjtLk4fmeSCFhNe9fFzLFj3m2B0YpA=
|
github.com/rancher/wrangler/v3 v3.0.0 h1:IHHCA+vrghJDPxjtLk4fmeSCFhNe9fFzLFj3m2B0YpA=
|
||||||
github.com/rancher/wrangler/v3 v3.0.0/go.mod h1:Dfckuuq7MJk2JWVBDywRlZXMxEyPxHy4XqGrPEzu5Eg=
|
github.com/rancher/wrangler/v3 v3.0.0/go.mod h1:Dfckuuq7MJk2JWVBDywRlZXMxEyPxHy4XqGrPEzu5Eg=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
@@ -6,8 +6,9 @@ import (
|
|||||||
|
|
||||||
var InsecureSkipTLSVerify bool
|
var InsecureSkipTLSVerify bool
|
||||||
var SystemDefaultRegistry string
|
var SystemDefaultRegistry string
|
||||||
|
var APIUIVersion = "1.1.11"
|
||||||
var ShellPodImage string
|
var ShellPodImage string
|
||||||
|
var BindAddress string
|
||||||
|
|
||||||
func Flags() []cli.Flag {
|
func Flags() []cli.Flag {
|
||||||
return []cli.Flag{
|
return []cli.Flag{
|
||||||
@@ -24,5 +25,16 @@ func Flags() []cli.Flag {
|
|||||||
Destination: &ShellPodImage,
|
Destination: &ShellPodImage,
|
||||||
Value: "rancher/shell:v0.2.1-rc.7",
|
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
|
||||||
|
)
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rancher/apiserver/pkg/types"
|
"github.com/rancher/apiserver/pkg/types"
|
||||||
|
"github.com/rancher/apiserver/pkg/urlbuilder"
|
||||||
steveauth "github.com/rancher/steve/pkg/auth"
|
steveauth "github.com/rancher/steve/pkg/auth"
|
||||||
"github.com/rancher/steve/pkg/schema"
|
"github.com/rancher/steve/pkg/schema"
|
||||||
"github.com/rancher/steve/pkg/server"
|
"github.com/rancher/steve/pkg/server"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/cnrancher/kube-explorer/internal/config"
|
"github.com/cnrancher/kube-explorer/internal/config"
|
||||||
"github.com/cnrancher/kube-explorer/internal/resources/cluster"
|
"github.com/cnrancher/kube-explorer/internal/resources/cluster"
|
||||||
"github.com/cnrancher/kube-explorer/internal/ui"
|
"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) {
|
func ToServer(ctx context.Context, c *cli.Config, sqlCache bool) (*server.Server, error) {
|
||||||
@@ -48,20 +50,31 @@ func ToServer(ctx context.Context, c *cli.Config, sqlCache bool) (*server.Server
|
|||||||
return nil, err
|
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{
|
steveServer, err := server.New(ctx, restConfig, &server.Options{
|
||||||
AuthMiddleware: auth,
|
AuthMiddleware: auth,
|
||||||
Controllers: controllers,
|
Controllers: controllers,
|
||||||
Next: ui.New(c.UIPath),
|
Next: ui,
|
||||||
SQLCache: sqlCache,
|
SQLCache: sqlCache,
|
||||||
// router needs to hack here
|
// router needs to hack here
|
||||||
Router: func(h router.Handlers) http.Handler {
|
Router: func(h router.Handlers) http.Handler {
|
||||||
return rewriteLocalCluster(router.Routes(h))
|
return handleProxyHeader(
|
||||||
|
rewriteLocalCluster(
|
||||||
|
router.Routes(h),
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
steveServer.APIServer.CustomAPIUIResponseWriter(apiui.CSS(), apiui.JS(), func() string { return config.APIUIVersion })
|
||||||
|
|
||||||
// registrer local cluster
|
// registrer local cluster
|
||||||
if err := cluster.Register(ctx, steveServer, c.Context); err != nil {
|
if err := cluster.Register(ctx, steveServer, c.Context); err != nil {
|
||||||
return steveServer, err
|
return steveServer, err
|
||||||
@@ -91,3 +104,12 @@ func rewriteLocalCluster(next http.Handler) http.Handler {
|
|||||||
next.ServeHTTP(rw, req)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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
|
@@ -4,88 +4,9 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// content holds our static web server content.
|
// content holds our static web server content.
|
||||||
//
|
//
|
||||||
//go:embed all:ui/*
|
//go:embed all:ui/*
|
||||||
var staticContent embed.FS
|
var staticContent embed.FS
|
||||||
|
|
||||||
type fsFunc func(name string) (fs.File, error)
|
|
||||||
|
|
||||||
func (f fsFunc) Open(name string) (fs.File, error) {
|
|
||||||
return f(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pathExist(path string) bool {
|
|
||||||
_, err := staticContent.Open(path)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openFile(path string) (fs.File, error) {
|
|
||||||
file, err := staticContent.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("openEmbedFile %s err: %v", path, err)
|
|
||||||
}
|
|
||||||
return file, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveEmbed(basePaths ...string) http.Handler {
|
|
||||||
handler := fsFunc(func(name string) (fs.File, error) {
|
|
||||||
logrus.Debugf("serveEmbed name: %s", name)
|
|
||||||
assetPath := joinEmbedFilepath(append(basePaths, name)...)
|
|
||||||
logrus.Debugf("serveEmbed final path: %s", assetPath)
|
|
||||||
return openFile(assetPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
return http.FileServer(http.FS(handler))
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveEmbedIndex(basePath string) http.Handler {
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
path := joinEmbedFilepath(basePath, "dashboard", "index.html")
|
|
||||||
logrus.Debugf("serveEmbedIndex : %s", path)
|
|
||||||
f, _ := staticContent.Open(path)
|
|
||||||
io.Copy(rw, f)
|
|
||||||
f.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Handler) ServeAsset() http.Handler {
|
|
||||||
return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
serveEmbed(u.pathSetting()).ServeHTTP(rw, req)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Handler) ServeFaviconDashboard() http.Handler {
|
|
||||||
return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
serveEmbed(u.pathSetting(), "dashboard").ServeHTTP(rw, req)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Handler) IndexFileOnNotFound() http.Handler {
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
path := joinEmbedFilepath(u.pathSetting(), req.URL.Path)
|
|
||||||
if pathExist(path) {
|
|
||||||
u.ServeAsset().ServeHTTP(rw, req)
|
|
||||||
} else {
|
|
||||||
u.IndexFile().ServeHTTP(rw, req)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Handler) IndexFile() http.Handler {
|
|
||||||
return u.indexMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
serveEmbedIndex(u.pathSetting()).ServeHTTP(rw, req)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinEmbedFilepath(paths ...string) string {
|
|
||||||
return filepath.ToSlash(filepath.Join(paths...))
|
|
||||||
}
|
|
||||||
|
@@ -1,42 +0,0 @@
|
|||||||
//go:build !embed
|
|
||||||
|
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *Handler) ServeAsset() http.Handler {
|
|
||||||
return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
http.FileServer(http.Dir(u.pathSetting())).ServeHTTP(rw, req)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Handler) ServeFaviconDashboard() http.Handler {
|
|
||||||
return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
http.FileServer(http.Dir(filepath.Join(u.pathSetting(), "dashboard"))).ServeHTTP(rw, req)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Handler) IndexFileOnNotFound() http.Handler {
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
// we ignore directories here because we want those to come from the CDN when running in that mode
|
|
||||||
if stat, err := os.Stat(filepath.Join(u.pathSetting(), req.URL.Path)); err == nil && !stat.IsDir() {
|
|
||||||
u.ServeAsset().ServeHTTP(rw, req)
|
|
||||||
} else {
|
|
||||||
u.IndexFile().ServeHTTP(rw, req)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Handler) IndexFile() http.Handler {
|
|
||||||
return u.indexMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
if path, isURL := u.path(); isURL {
|
|
||||||
_ = serveIndex(rw, path)
|
|
||||||
} else {
|
|
||||||
http.ServeFile(rw, req, filepath.Join(path, "index.html"))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
@@ -1,43 +1,30 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
|
|
||||||
|
"github.com/cnrancher/kube-explorer/internal/ui/content"
|
||||||
"github.com/rancher/apiserver/pkg/middleware"
|
"github.com/rancher/apiserver/pkg/middleware"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
defaultPath = "./ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
insecureClient = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type StringSetting func() string
|
type StringSetting func() string
|
||||||
type BoolSetting func() bool
|
type BoolSetting func() bool
|
||||||
|
|
||||||
|
func StaticSetting[T any](input T) func() T {
|
||||||
|
return func() T {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
contentHandlers map[string]content.Handler
|
||||||
pathSetting func() string
|
pathSetting func() string
|
||||||
indexSetting func() string
|
indexSetting func() string
|
||||||
releaseSetting func() bool
|
releaseSetting func() bool
|
||||||
offlineSetting func() string
|
offlineSetting func() string
|
||||||
middleware func(http.Handler) http.Handler
|
middleware func(http.Handler) http.Handler
|
||||||
indexMiddleware func(http.Handler) http.Handler
|
indexMiddleware func(http.Handler) http.Handler
|
||||||
|
|
||||||
downloadOnce sync.Once
|
|
||||||
downloadSuccess bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -45,7 +32,7 @@ type Options struct {
|
|||||||
Path StringSetting
|
Path StringSetting
|
||||||
// The HTTP URL of the index file to download
|
// The HTTP URL of the index file to download
|
||||||
Index StringSetting
|
Index StringSetting
|
||||||
// Whether or not to run the UI offline, should return true/false/dynamic
|
// Whether or not to run the UI offline, should return true/false/dynamic/embed
|
||||||
Offline StringSetting
|
Offline StringSetting
|
||||||
// Whether or not is it release, if true UI will run offline if set to dynamic
|
// Whether or not is it release, if true UI will run offline if set to dynamic
|
||||||
ReleaseSetting BoolSetting
|
ReleaseSetting BoolSetting
|
||||||
@@ -57,10 +44,11 @@ func NewUIHandler(opts *Options) *Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
indexSetting: opts.Index,
|
contentHandlers: make(map[string]content.Handler),
|
||||||
offlineSetting: opts.Offline,
|
indexSetting: opts.Index,
|
||||||
pathSetting: opts.Path,
|
offlineSetting: opts.Offline,
|
||||||
releaseSetting: opts.ReleaseSetting,
|
pathSetting: opts.Path,
|
||||||
|
releaseSetting: opts.ReleaseSetting,
|
||||||
middleware: middleware.Chain{
|
middleware: middleware.Chain{
|
||||||
middleware.Gzip,
|
middleware.Gzip,
|
||||||
middleware.FrameOptions,
|
middleware.FrameOptions,
|
||||||
@@ -75,67 +63,75 @@ func NewUIHandler(opts *Options) *Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if h.indexSetting == nil {
|
if h.indexSetting == nil {
|
||||||
h.indexSetting = func() string {
|
h.indexSetting = StaticSetting("")
|
||||||
return "https://releases.rancher.com/dashboard/latest/index.html"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.offlineSetting == nil {
|
if h.offlineSetting == nil {
|
||||||
h.offlineSetting = func() string {
|
h.offlineSetting = StaticSetting("dynamic")
|
||||||
return "dynamic"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.pathSetting == nil {
|
if h.pathSetting == nil {
|
||||||
h.pathSetting = func() string {
|
h.pathSetting = StaticSetting("")
|
||||||
return defaultPath
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.releaseSetting == nil {
|
if h.releaseSetting == nil {
|
||||||
h.releaseSetting = func() bool {
|
h.releaseSetting = StaticSetting(false)
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.contentHandlers["embed"] = content.NewEmbedded(staticContent, "ui")
|
||||||
|
h.contentHandlers["false"] = content.NewExternal(h.indexSetting)
|
||||||
|
h.contentHandlers["true"] = content.NewFilepath(h.pathSetting)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Handler) path() (path string, isURL bool) {
|
func (h *Handler) content() content.Handler {
|
||||||
switch u.offlineSetting() {
|
offline := h.offlineSetting()
|
||||||
case "dynamic":
|
if handler, ok := h.contentHandlers[offline]; ok {
|
||||||
if u.releaseSetting() {
|
return handler
|
||||||
return u.pathSetting(), false
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
if u.canDownload(u.indexSetting()) {
|
fallthrough
|
||||||
return u.indexSetting(), true
|
case h.releaseSetting():
|
||||||
}
|
// release must use embed first
|
||||||
return u.pathSetting(), false
|
return embedHandler
|
||||||
case "true":
|
|
||||||
return u.pathSetting(), false
|
|
||||||
default:
|
default:
|
||||||
return u.indexSetting(), true
|
// try embed
|
||||||
}
|
if _, err := embedHandler.GetIndex(); err == nil {
|
||||||
}
|
return embedHandler
|
||||||
|
|
||||||
func (u *Handler) canDownload(url string) bool {
|
|
||||||
u.downloadOnce.Do(func() {
|
|
||||||
if err := serveIndex(io.Discard, url); err == nil {
|
|
||||||
u.downloadSuccess = true
|
|
||||||
} else {
|
|
||||||
logrus.Errorf("Failed to download %s, falling back to packaged UI", url)
|
|
||||||
}
|
}
|
||||||
})
|
return externalHandler
|
||||||
return u.downloadSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
func (h *Handler) ServeAssets(next http.Handler) http.Handler {
|
||||||
return err
|
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)
|
||||||
|
}
|
@@ -4,29 +4,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cnrancher/kube-explorer/internal/version"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(path string) http.Handler {
|
func New(opt *Options) (http.Handler, APIUI) {
|
||||||
vue := NewUIHandler(&Options{
|
vue := NewUIHandler(opt)
|
||||||
Path: func() string {
|
|
||||||
if path == "" {
|
|
||||||
return defaultPath
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
},
|
|
||||||
Offline: func() string {
|
|
||||||
if path != "" {
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
return "dynamic"
|
|
||||||
},
|
|
||||||
ReleaseSetting: func() bool {
|
|
||||||
return version.IsRelease()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
router.UseEncodedPath()
|
router.UseEncodedPath()
|
||||||
|
|
||||||
@@ -35,7 +17,8 @@ func New(path string) http.Handler {
|
|||||||
router.Handle("/dashboard/", vue.IndexFile())
|
router.Handle("/dashboard/", vue.IndexFile())
|
||||||
router.Handle("/favicon.png", vue.ServeFaviconDashboard())
|
router.Handle("/favicon.png", vue.ServeFaviconDashboard())
|
||||||
router.Handle("/favicon.ico", vue.ServeFaviconDashboard())
|
router.Handle("/favicon.ico", vue.ServeFaviconDashboard())
|
||||||
router.PathPrefix("/dashboard/").Handler(vue.IndexFileOnNotFound())
|
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) {
|
router.PathPrefix("/k8s/clusters/local").HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
url := strings.TrimPrefix(req.URL.Path, "/k8s/clusters/local")
|
url := strings.TrimPrefix(req.URL.Path, "/k8s/clusters/local")
|
||||||
if url == "" {
|
if url == "" {
|
||||||
@@ -44,5 +27,5 @@ func New(path string) http.Handler {
|
|||||||
http.Redirect(rw, req, url, http.StatusFound)
|
http.Redirect(rw, req, url, http.StatusFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
return router
|
return proxyMiddleware(router), apiUI(opt)
|
||||||
}
|
}
|
||||||
|
17
main.go
17
main.go
@@ -3,9 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/cnrancher/kube-explorer/internal/version"
|
||||||
"github.com/rancher/steve/pkg/debug"
|
"github.com/rancher/steve/pkg/debug"
|
||||||
stevecli "github.com/rancher/steve/pkg/server/cli"
|
stevecli "github.com/rancher/steve/pkg/server/cli"
|
||||||
"github.com/rancher/steve/pkg/version"
|
|
||||||
"github.com/rancher/wrangler/v3/pkg/signals"
|
"github.com/rancher/wrangler/v3/pkg/signals"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
@@ -14,19 +14,14 @@ import (
|
|||||||
"github.com/cnrancher/kube-explorer/internal/server"
|
"github.com/cnrancher/kube-explorer/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
config stevecli.Config
|
|
||||||
debugconfig debug.Config
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "kube-explorer"
|
app.Name = "kube-explorer"
|
||||||
app.Version = version.FriendlyVersion()
|
app.Version = version.FriendlyVersion()
|
||||||
app.Usage = ""
|
app.Usage = ""
|
||||||
app.Flags = joinFlags(
|
app.Flags = joinFlags(
|
||||||
stevecli.Flags(&config),
|
stevecli.Flags(&keconfig.Steve),
|
||||||
debug.Flags(&debugconfig),
|
debug.Flags(&keconfig.Debug),
|
||||||
keconfig.Flags(),
|
keconfig.Flags(),
|
||||||
)
|
)
|
||||||
app.Action = run
|
app.Action = run
|
||||||
@@ -38,12 +33,12 @@ func main() {
|
|||||||
|
|
||||||
func run(_ *cli.Context) error {
|
func run(_ *cli.Context) error {
|
||||||
ctx := signals.SetupSignalContext()
|
ctx := signals.SetupSignalContext()
|
||||||
debugconfig.MustSetupDebug()
|
keconfig.Debug.MustSetupDebug()
|
||||||
s, err := server.ToServer(ctx, &config, false)
|
s, err := server.ToServer(ctx, &keconfig.Steve, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.ListenAndServe(ctx, config.HTTPSListenPort, config.HTTPListenPort, nil)
|
return server.Serve(ctx, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinFlags(flags ...[]cli.Flag) []cli.Flag {
|
func joinFlags(flags ...[]cli.Flag) []cli.Flag {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
FROM registry.suse.com/bci/bci-minimal:15.6
|
FROM registry.suse.com/bci/bci-base:15.6
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ENV ARCH=${TARGETARCH:-"amd64"} OS=${TARGETOS:-"linux"}
|
ENV ARCH=${TARGETARCH:-"amd64"} OS=${TARGETOS:-"linux"}
|
||||||
COPY entrypoint.sh /usr/bin/
|
RUN zypper install -y catatonit
|
||||||
COPY kube-explorer-${OS}-${ARCH} /usr/bin/kube-explorer
|
COPY kube-explorer-${OS}-${ARCH} /usr/bin/kube-explorer
|
||||||
ENTRYPOINT ["entrypoint.sh"]
|
ENTRYPOINT [ "/usr/bin/catatonit", "--", "kube-explorer" ]
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
kube-explorer "${@}"
|
|
@@ -6,76 +6,33 @@ cd "$(dirname $0)/.."
|
|||||||
|
|
||||||
rm -rf ./bin/* ./dist/*
|
rm -rf ./bin/* ./dist/*
|
||||||
|
|
||||||
OS_ARCH_ARG_LINUX="amd64 arm arm64"
|
BUILD_TARGET="${BUILD_TARGET:-dev}"
|
||||||
OS_ARCH_ARG_DARWIN="amd64 arm64"
|
CROSS=${CROSS:-}
|
||||||
OS_ARCH_ARG_WINDOWS="amd64"
|
|
||||||
|
|
||||||
LD_INJECT_VALUES="-X github.com/cnrancher/kube-explorer/internal/version.Version=$VERSION
|
if [[ ${GITHUB_REF} == refs/tags/* ]]; then
|
||||||
-X github.com/cnrancher/kube-explorer/internal/version.GitCommit=$COMMIT"
|
CROSS=tag
|
||||||
|
elif [ -n "${GITHUB_REF}" ]; then
|
||||||
|
CROSS=push
|
||||||
|
fi
|
||||||
|
|
||||||
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
|
case "$CROSS" in
|
||||||
|
|
||||||
case "$CROSS" in
|
|
||||||
"push")
|
|
||||||
for ARCH in ${OS_ARCH_ARG_LINUX}; do
|
|
||||||
OUTPUT_BIN="bin/kube-explorer-linux-$ARCH"
|
|
||||||
echo "Building binary for linux/$ARCH..."
|
|
||||||
GOARCH=$ARCH GOOS=linux CGO_ENABLED=0 go build -tags embed \
|
|
||||||
-ldflags \
|
|
||||||
"$LD_INJECT_VALUES $LINKFLAGS" \
|
|
||||||
-o ${OUTPUT_BIN}
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
"tag")
|
"tag")
|
||||||
for ARCH in ${OS_ARCH_ARG_LINUX}; do
|
BUILD_TARGET="prod"
|
||||||
OUTPUT_BIN="bin/kube-explorer-linux-$ARCH"
|
;;
|
||||||
echo "Building binary for linux/$ARCH..."
|
"push")
|
||||||
GOARCH=$ARCH GOOS=linux CGO_ENABLED=0 go build -tags embed \
|
;;
|
||||||
-ldflags \
|
|
||||||
"$LD_INJECT_VALUES $LINKFLAGS" \
|
|
||||||
-o ${OUTPUT_BIN}
|
|
||||||
done
|
|
||||||
|
|
||||||
for ARCH in ${OS_ARCH_ARG_DARWIN}; do
|
|
||||||
OUTPUT_BIN="bin/kube-explorer-darwin-$ARCH"
|
|
||||||
echo "Building binary for darwin/$ARCH..."
|
|
||||||
GOARCH=$ARCH GOOS=darwin CGO_ENABLED=0 go build -tags embed \
|
|
||||||
-ldflags \
|
|
||||||
"$LD_INJECT_VALUES" \
|
|
||||||
-o ${OUTPUT_BIN}
|
|
||||||
done
|
|
||||||
|
|
||||||
for ARCH in ${OS_ARCH_ARG_WINDOWS}; do
|
|
||||||
OUTPUT_BIN="bin/kube-explorer-windows-$ARCH.exe"
|
|
||||||
echo "Building binary for windows/$ARCH..."
|
|
||||||
GOARCH=$ARCH GOOS=windows CGO_ENABLED=0 go build -tags embed \
|
|
||||||
-ldflags \
|
|
||||||
"$LD_INJECT_VALUES" \
|
|
||||||
-o ${OUTPUT_BIN}
|
|
||||||
done
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
# only build one for current platform
|
BUILD_ARG="${BUILD_ARG} --single-target"
|
||||||
CGO_ENABLED=0 go build -tags embed \
|
;;
|
||||||
-ldflags \
|
|
||||||
"$LD_INJECT_VALUES $LINKFLAGS" \
|
|
||||||
-o "bin/kube-explorer-$(uname | tr '[:upper:]' '[:lower:]')-${ARCH}"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
BUILD_ARG="${BUILD_ARG:-} --skip validate --id ${BUILD_TARGET}"
|
||||||
|
|
||||||
mkdir -p "./bin"
|
mkdir -p "./bin"
|
||||||
|
|
||||||
|
# upx is handled by goreleaser
|
||||||
|
VERSION=${VERSION} COMMIT=${COMMIT} goreleaser build $BUILD_ARG
|
||||||
|
|
||||||
mkdir -p "./dist"
|
mkdir -p "./dist"
|
||||||
|
|
||||||
for f in ./bin/*; do
|
cp -r bin/kube-explorer-* dist/
|
||||||
filename=$(basename "$f")
|
|
||||||
if [[ $filename != *darwin* && -z "$SKIP_COMPRESS" ]]; then
|
|
||||||
if upx -o "./dist/$filename" "$f"; then
|
|
||||||
echo "UPX done for $filename!"
|
|
||||||
else
|
|
||||||
echo "UPX failed for $filename, copying original file."
|
|
||||||
cp "$f" "./dist/$filename"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
cp "$f" "./dist/$filename"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
@@ -6,4 +6,6 @@ cd $(dirname $0)
|
|||||||
./download
|
./download
|
||||||
./validate
|
./validate
|
||||||
./build
|
./build
|
||||||
./package
|
if [ -z "${SKIP_PACKAGE}" ]; then
|
||||||
|
./package
|
||||||
|
fi
|
@@ -1,8 +1,10 @@
|
|||||||
#!/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"
|
||||||
|
|
||||||
|
@@ -10,9 +10,13 @@ else
|
|||||||
TAR_CMD="tar"
|
TAR_CMD="tar"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rm -rf internal/ui/ui/*
|
||||||
|
|
||||||
mkdir -p internal/ui/ui/dashboard
|
mkdir -p internal/ui/ui/dashboard
|
||||||
cd internal/ui/ui/dashboard || exit 1;
|
cd internal/ui/ui/dashboard || exit 1;
|
||||||
curl -sL https://pandaria-dashboard-ui.s3.ap-southeast-2.amazonaws.com/release-2.8-cn/kube-explorer-ui/${CATTLE_DASHBOARD_UI_VERSION}.tar.gz | $TAR_CMD xvzf - --strip-components=2
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cd $(dirname $0)
|
|
||||||
|
|
||||||
./download
|
|
||||||
./validate
|
|
||||||
./build
|
|
||||||
|
|
@@ -18,3 +18,14 @@ go mod tidy
|
|||||||
|
|
||||||
echo Verifying modules
|
echo Verifying modules
|
||||||
go mod verify
|
go mod verify
|
||||||
|
|
||||||
|
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
|
#!/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
|
||||||
|
Reference in New Issue
Block a user