Compare commits

...

14 Commits

Author SHA1 Message Date
hongming
4efc7c9fbe Update build-multiarch.yaml (#6213)
Signed-off-by: hongming <hongming@kubesphere.io>
2024-09-26 21:41:47 +08:00
hongming
3abf00b7c1 Release v4.1.2 (#6212)
Signed-off-by: hongming <coder.scala@gmail.com>
2024-09-26 21:02:26 +08:00
hongming
31ee299312 Update sync-helm-chart.yaml
Signed-off-by: hongming <hongming@kubesphere.io>
2024-09-26 18:16:01 +08:00
KubeSphere CI Bot
3187fcc173 [release-4.1] fix: remove the incorrect RBAC rule merging logic (#6210)
fix: remove the incorrect RBAC rule merging logic

Signed-off-by: hongming <coder.scala@gmail.com>
Co-authored-by: hongming <coder.scala@gmail.com>
2024-09-26 16:18:17 +08:00
KubeSphere CI Bot
f0ab0b9856 [release-4.1] adjust the log level of unexpected exceptions (#6208)
adjust the log level of unexpected exceptions

Signed-off-by: hongming <coder.scala@gmail.com>
Co-authored-by: hongming <coder.scala@gmail.com>
2024-09-26 11:58:17 +08:00
KubeSphere CI Bot
7e703750e8 [release-4.1] feat: Adapt to oci-based helmchart repo (#6203)
* add oci client for registry

* add LoadRepoIndexFormOci

* feat: Adapt to oci-based helmchart repo

* Update the golang base image version in the dockerfile

* update oci_test.go

Signed-off-by: lingbo <lingbo@lingbohome.com>

* fix: Update oci_test.go

Signed-off-by: 凌波 <lingbo@lingbohome.com>

* Update go imports

---------

Signed-off-by: lingbo <lingbo@lingbohome.com>
Signed-off-by: 凌波 <lingbo@lingbohome.com>
Co-authored-by: lingbo <lingbo@lingbohome.com>
Co-authored-by: hongming <coder.scala@gmail.com>
2024-09-25 11:02:16 +08:00
KubeSphere CI Bot
88db498bcd [release-4.1] fix: add annotation checksum/cert to extensions-museum pod (#6202)
fix: add annotation checksum/cert to extensions-museum pod

Signed-off-by: joyceliu <joyceliu@yunify.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
2024-09-24 16:54:15 +08:00
KubeSphere CI Bot
f290167267 [release-4.1] fix: add tls when get repository index. (#6198)
* fix: add tls when get repository index.

Signed-off-by: joyceliu <joyceliu@yunify.com>

* Update staging/src/kubesphere.io/utils/helm/repo_index.go

Signed-off-by: hongming <coder.scala@gmail.com>

* fix: add tls when get repository index.

Signed-off-by: joyceliu <joyceliu@yunify.com>

---------

Signed-off-by: joyceliu <joyceliu@yunify.com>
Signed-off-by: hongming <coder.scala@gmail.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
Co-authored-by: hongming <coder.scala@gmail.com>
2024-09-23 15:28:14 +08:00
KubeSphere CI Bot
0a06cd8a1b [release-4.1] fix: graceful delete ks-core (#6189)
* fix: graceful delete ks-core

Signed-off-by: joyceliu <joyceliu@yunify.com>

* fix: graceful delete ks-core

Signed-off-by: joyceliu <joyceliu@yunify.com>

---------

Signed-off-by: joyceliu <joyceliu@yunify.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
2024-09-19 18:50:09 +08:00
KubeSphere CI Bot
0a21a58582 [release-4.1] fix: cronjob.batch/v1 is invalid in k8s v1.19 (#6188)
fix: auto update extension museum only when kubernetes version > =v1.20.0

Signed-off-by: joyceliu <joyceliu@yunify.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
2024-09-19 18:45:10 +08:00
KubeSphere CI Bot
de786c4b84 [release-4.1] Use responsewriter.UserProvidedDecorator instead of auto flush response (#6175)
Use responsewriter.UserProvidedDecorator instead of auto flush response

Signed-off-by: hongming <coder.scala@gmail.com>
Co-authored-by: hongming <coder.scala@gmail.com>
2024-09-13 17:13:02 +08:00
KubeSphere CI Bot
dad35f7389 [release-4.1] feat: add extension-museum in helm (#6174)
* feat: add extension-museum in helm

Signed-off-by: joyceliu <joyceliu@yunify.com>

* feat: add extension-museum in helm

Signed-off-by: joyceliu <joyceliu@yunify.com>

---------

Signed-off-by: joyceliu <joyceliu@yunify.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
2024-09-13 16:54:02 +08:00
KubeSphere CI Bot
7dcb5d9d3b [release-4.1] Update telemetry config (#6172)
Update telemetry config

Signed-off-by: hongming <coder.scala@gmail.com>
Co-authored-by: hongming <coder.scala@gmail.com>
2024-09-13 16:49:02 +08:00
KubeSphere CI Bot
14d48c9267 [release-4.1] fix typo (#1965) (#6173)
fix typo (#1965)

Co-authored-by: inksnw <inksnw@gmail.com>
2024-09-13 16:42:05 +08:00
46 changed files with 1343 additions and 207 deletions

View File

@@ -17,16 +17,16 @@ jobs:
GO111MODULE: on
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Login to Aliyun
- name: Login to HUAWEICLOUD
uses: docker/login-action@v3
with:
registry: registry.cn-beijing.aliyuncs.com
username: ${{ secrets.ALIYUNCS_USERNAME }}
password: ${{ secrets.ALIYUNCS_PASSWORD }}
registry: swr.cn-southwest-2.myhuaweicloud.com
username: ${{ secrets.HUAWEICLOUD_USERNAME }}
password: ${{ secrets.HUAWEICLOUD_PASSWORD }}
- name: Login to DOCKER
uses: docker/login-action@v3
@@ -56,17 +56,18 @@ jobs:
uses: docker/build-push-action@v6
if: steps.chose_registry.outputs.env == 'prod'
with:
context: ${{ github.workspace }}
file: build/ks-apiserver/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
docker.io/kubesphere/ks-apiserver:${{ steps.chose_registry.outputs.tag }}
registry.cn-beijing.aliyuncs.com/kubesphereio/ks-apiserver:${{ steps.chose_registry.outputs.tag }}
- name: Build and push ks-apiserver dev images
uses: docker/build-push-action@v6
if: steps.chose_registry.outputs.env == 'dev'
with:
context: ${{ github.workspace }}
file: build/ks-apiserver/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
@@ -77,19 +78,34 @@ jobs:
uses: docker/build-push-action@v6
if: steps.chose_registry.outputs.env == 'prod'
with:
context: ${{ github.workspace }}
file: build/ks-controller-manager/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
docker.io/kubesphere/ks-controller-manager:${{ steps.chose_registry.outputs.tag }}
registry.cn-beijing.aliyuncs.com/kubesphereio/ks-controller-manager:${{ steps.chose_registry.outputs.tag }}
- name: Build and push ks-controller-manager dev images
uses: docker/build-push-action@v6
if: steps.chose_registry.outputs.env == 'dev'
with:
context: ${{ github.workspace }}
file: build/ks-controller-manager/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
docker.io/kubespheredev/ks-controller-manager:${{ steps.chose_registry.outputs.tag }}
- name: Sync ks images to HUAWEICLOUD
if: steps.chose_registry.outputs.env == 'prod'
run: |
# apiserver
docker pull docker.io/kubesphere/ks-apiserver:${{ steps.chose_registry.outputs.tag }}
docker tag docker.io/kubesphere/ks-apiserver:${{ steps.chose_registry.outputs.tag }} swr.cn-southwest-2.myhuaweicloud.com/ks/kubesphere/ks-apiserver:${{ steps.chose_registry.outputs.tag }}
docker push swr.cn-southwest-2.myhuaweicloud.com/ks/kubesphere/ks-apiserver:${{ steps.chose_registry.outputs.tag }}
# controller-manager
docker pull docker.io/kubesphere/ks-controller-manager:${{ steps.chose_registry.outputs.tag }}
docker tag docker.io/kubesphere/ks-controller-manager:${{ steps.chose_registry.outputs.tag }} swr.cn-southwest-2.myhuaweicloud.com/ks/kubesphere/ks-controller-manager:${{ steps.chose_registry.outputs.tag }}
docker push swr.cn-southwest-2.myhuaweicloud.com/ks/kubesphere/ks-controller-manager:${{ steps.chose_registry.outputs.tag }}

View File

@@ -1,3 +1,5 @@
name: SyncHelmChart
on:
workflow_dispatch:
push:
@@ -5,36 +7,64 @@ on:
- 'config/ks-core/**'
branches:
- 'master'
- 'release-*'
tags:
- 'helm-chart-*'
- 'v*'
jobs:
sync-chart:
runs-on: self-runner-kubesphere
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Check out kubesphere/kubesphere
uses: actions/checkout@v4
with:
path: kubesphere
fetch-depth: 0
- name: Check out kubesphere/helm-charts
uses: actions/checkout@v4
with:
repository: ks-ci-bot/helm-charts
token: ${{ secrets.CIBOT_ACCESS_TOKEN }}
path: helm-charts
fetch-depth: 0
- name: Setup SSH
uses: MrSquaare/ssh-setup-action@v2
uses: MrSquaare/ssh-setup-action@v3
with:
working-directory: helm-charts
host: github.com
private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- run: |
- name: Sync charts
run: |
cd helm-charts
git config --global user.email "ci-bot@kubesphere.io"
git config --global user.name "ks-ci-bot"
git clone git@github.com:kubesphere/helm-charts.git
rm -rf helm-charts/src/test/ks-core
cp -r config/ks-core helm-charts/src/test/
cd helm-charts/
git remote add ks git@github.com:kubesphere/helm-charts.git
git fetch ks master
git checkout -b sync/ks-core/${GITHUB_REF#refs/*/} ks/master
rm -rf src/test/ks-core
cp -r ../kubesphere/config/ks-core src/test/
if [[ ${GITHUB_REF#refs/*/} =~ ^helm-chart-[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
rm -rf src/main/ks-core
cp -r ../kubesphere/config/ks-core src/main/
fi
git add .
git commit -m "update ks-core helm chart"
git push origin master:sync/ks-core --force
git push origin sync/ks-core/${GITHUB_REF#refs/*/} --force
- env:
- name: Create Pull Request
env:
GH_TOKEN: ${{ secrets.CIBOT_ACCESS_TOKEN }}
run: |
cd helm-charts/
if [[ $(gh pr ls -H sync/ks-core -B master) == "" ]]; then
gh pr create -H sync/ks-core -B master --title "Update ks-core helm chart" --body "Update ks-core helm chart"
cd helm-charts
if [[ $(gh pr ls -R kubesphere/helm-charts -A ks-ci-bot -H sync/ks-core/${GITHUB_REF#refs/*/} -B master) == "" ]]; then
# Create a PR in the kubesphere/helm-charts repository
gh pr create -R kubesphere/helm-charts \
-B master \
-H ks-ci-bot:sync/ks-core/${GITHUB_REF#refs/*/} \
-t "Update ks-core helm chart from ${GITHUB_REF#refs/*/}" \
-b "Update ks-core helm chart"
fi

View File

@@ -2,9 +2,6 @@
CRD_OPTIONS ?= "crd:allowDangerousTypes=true"
MANIFESTS="cluster/v1alpha1 iam/... quota/v1alpha2 storage/v1alpha1 tenant/... extensions/v1alpha1 core/v1alpha1 gateway/v1alpha2 application/v2"
# App Version
APP_VERSION = v3.2.0
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
@@ -112,19 +109,15 @@ container-cross-push: ; $(info $(M)...Begin to build and push.) @ ## Build and
hack/docker_build_multiarch.sh
helm-package: ; $(info $(M)...Begin to helm-package.) @ ## Helm-package.
ls config/crds/ | xargs -i cp -r config/crds/{} config/ks-core/crds/
helm package config/ks-core --app-version=${APP_VERSION} --version=0.1.0 -d ./bin
helm package config/ks-core -d ./bin
helm-deploy: ; $(info $(M)...Begin to helm-deploy.) @ ## Helm-deploy.
ls config/crds/ | xargs -i cp -r config/crds/{} config/ks-core/crds/
- kubectl create ns kubesphere-controls-system
helm upgrade --install ks-core ./config/ks-core -n kubesphere-system --create-namespace
kubectl apply -f https://raw.githubusercontent.com/kubesphere/ks-installer/master/roles/ks-core/prepare/files/ks-init/role-templates.yaml
helm-uninstall: ; $(info $(M)...Begin to helm-uninstall.) @ ## Helm-uninstall.
- kubectl delete ns kubesphere-controls-system
helm uninstall ks-core -n kubesphere-system
kubectl delete -f https://raw.githubusercontent.com/kubesphere/ks-installer/master/roles/ks-core/prepare/files/ks-init/role-templates.yaml
# Run tests
test: vet test-env ;$(info $(M)...Begin to run tests.) @ ## Run tests.

View File

@@ -1,5 +1,5 @@
# Build
FROM golang:1.20.7 AS build_context
FROM golang:1.21.5 AS build_context
ENV OUTDIR=/out
RUN mkdir -p ${OUTDIR}/usr/local/bin/

View File

@@ -16,7 +16,7 @@ RUN curl -LO https://github.com/kubesphere/telemetry/releases/download/v1.0.0/te
COPY config/ks-core ${OUTDIR}/var/helm-charts/ks-core
# Build
FROM golang:1.20.7 AS build_context
FROM golang:1.21.5 AS build_context
ENV OUTDIR=/out
RUN mkdir -p ${OUTDIR}/usr/local/bin/

View File

@@ -7,12 +7,12 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.1.0
version: 1.1.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: "v4.1.1"
appVersion: "v4.1.2"
dependencies:
- name: redis-ha

View File

@@ -73,6 +73,8 @@ spec:
type: string
appVersionID:
type: string
icon:
type: string
values:
format: byte
type: string

View File

@@ -50,12 +50,18 @@ spec:
type: string
type: object
caBundle:
description: if the caBundle is empty, use --insecure-skip-tls-verify.
description: The caBundle (base64 string) is used in helmExecutor
to verify the helm server.
type: string
description:
type: string
image:
description: 'DEPRECATED: the field will remove in future versions,
please use url.'
type: string
insecure:
description: --insecure-skip-tls-verify. default false
type: boolean
updateStrategy:
properties:
registryPoll:

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# set -x
CRD_NAMES=$1
MAPPING_CONFIG=$2
for extension in `kubectl get installplan -o json | jq -r '.items[] | select(.status.state == "Installed") | .metadata.name'`
do
namespace=$(kubectl get installplan $extension -o=jsonpath='{.status.targetNamespace}')
version=$(kubectl get extension $extension -o=jsonpath='{.status.installedVersion}')
extensionversion=$extension-$version
echo "Found extension $extensionversion installed"
helm status $extension --namespace $namespace
if [ $? -eq 0 ]; then
helm mapkubeapis $extension --namespace $namespace --mapfile $MAPPING_CONFIG
fi
helm status $extension-agent --namespace $namespace
if [ $? -eq 0 ]; then
helm mapkubeapis $extension-agent --namespace $namespace --mapfile $MAPPING_CONFIG
fi
done
# remove namespace's finalizers && ownerReferences
kubectl patch workspaces.tenant.kubesphere.io system-workspace -p '{"metadata":{"finalizers":[]}}' --type=merge
kubectl patch workspacetemplates.tenant.kubesphere.io system-workspace -p '{"metadata":{"finalizers":[]}}' --type=merge
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}' -l 'kubesphere.io/managed=true')
do
kubectl label ns $ns kubesphere.io/workspace- && \
kubectl patch ns $ns -p '{"metadata":{"ownerReferences":[]}}' --type=merge && \
echo "{\"kind\":\"Namespace\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"$ns\",\"finalizers\":null}}" | kubectl replace --raw "/api/v1/namespaces/$ns/finalize" -f -
done
# delete crds
for crd in `kubectl get crds -o jsonpath="{.items[*].metadata.name}"`
do
if [[ ${CRD_NAMES[@]/${crd}/} != ${CRD_NAMES[@]} ]]; then
scop=$(eval echo $(kubectl get crd ${crd} -o jsonpath="{.spec.scope}"))
if [[ $scop =~ "Namespaced" ]] ; then
kubectl get $crd -A --no-headers | awk '{print $1" "$2" ""'$crd'"}' | xargs -n 3 sh -c 'kubectl patch $2 -n $0 $1 -p "{\"metadata\":{\"finalizers\":null}}" --type=merge 2>/dev/null && kubectl delete $2 -n $0 $1 2>/dev/null'
else
kubectl get $crd -A --no-headers | awk '{print $1" ""'$crd'"}' | xargs -n 2 sh -c 'kubectl patch $1 $0 -p "{\"metadata\":{\"finalizers\":null}}" --type=merge 2>/dev/null && kubectl delete $1 $0 2>/dev/null'
fi
kubectl delete crd $crd 2>/dev/null;
fi
done

View File

@@ -0,0 +1,21 @@
{{- define "kubectl.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.kubectl.image "global" (default .Values.global (dict "imageRegistry" "docker.io"))) }}
{{- end -}}
{{- define "common.images.image" -}}
{{- $registryName := .global.imageRegistry -}}
{{- $repositoryName := .imageRoot.repository -}}
{{- $separator := ":" -}}
{{- $termination := .global.tag | toString -}}
{{- if .imageRoot.registry }}
{{- $registryName = .imageRoot.registry -}}
{{- end -}}
{{- if .imageRoot.tag }}
{{- $termination = .imageRoot.tag | toString -}}
{{- end -}}
{{- if .imageRoot.digest }}
{{- $separator = "@" -}}
{{- $termination = .imageRoot.digest | toString -}}
{{- end -}}
{{- printf "%s/%s%s%s" $registryName $repositoryName $separator $termination -}}
{{- end -}}

View File

@@ -0,0 +1,89 @@
{{- $kubeVersion := .Capabilities.KubeVersion }}
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ .Release.Name }}-post-delete-crd-scripts"
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-weight": "-3"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed
data:
map.yaml: |
mappings:
{{- range $path, $_ := .Files.Glob "crds/**" }}
{{- $crd := $.Files.Get $path | fromYaml }}
{{- range $_, $version := $crd.spec.versions }}
- deprecatedAPI: "apiVersion: {{ $crd.spec.group }}/{{ $version.name }}\nkind: {{ $crd.spec.names.kind }}\n"
removedInVersion: "{{ $kubeVersion }}"
{{- end }}
{{- end }}
{{ (.Files.Glob "scripts/post-delete.sh").AsConfig | indent 2 }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: "{{ .Release.Name }}-post-delete-crd"
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-weight": "-3"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: "{{ .Release.Name }}-post-delete-crd"
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-weight": "-3"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: "{{ .Release.Name }}-post-delete-crd"
namespace: {{ .Release.Namespace }}
---
{{- $crdNameList := list }}
{{- range $path, $_ := .Files.Glob "crds/**" }}
{{- $crd := $.Files.Get $path | fromYaml }}
{{- $crdNameList = append $crdNameList $crd.metadata.name }}
{{- end }}
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ .Release.Name }}-post-delete-crd"
annotations:
"helm.sh/hook": post-delete
"helm.sh/hook-weight": "-2"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
serviceAccountName: "{{ .Release.Name }}-post-delete-crd"
containers:
- name: post-delete-job
image: {{ template "kubectl.image" . }}
command:
- /bin/bash
- /scripts/post-delete.sh
- '{{ join " " $crdNameList }}'
- /scripts/map.yaml
volumeMounts:
- mountPath: /scripts
name: scripts
resources: {{- toYaml .Values.kubectl.resources | nindent 12 }}
volumes:
- name: scripts
configMap:
name: "{{ .Release.Name }}-post-delete-crd-scripts"
defaultMode: 420

View File

@@ -10,6 +10,36 @@ data:
{{ (.Files.Glob "scripts/install.sh").AsConfig | indent 2 }}
{{ (.Files.Glob "crds/*").AsConfig | indent 2 }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: "{{ .Release.Name }}-pre-upgrade-crd"
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "-1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: "{{ .Release.Name }}-pre-upgrade-crd"
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "-1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: "{{ .Release.Name }}-pre-upgrade-crd"
namespace: {{ .Release.Namespace }}
---
apiVersion: batch/v1
kind: Job
@@ -23,10 +53,10 @@ spec:
template:
spec:
restartPolicy: Never
serviceAccountName: {{ include "ks-core.serviceAccountName" . }}
serviceAccountName: "{{ .Release.Name }}-pre-upgrade-crd"
containers:
- name: crd-install
image: {{ template "preUpgrade.image" . }}
image: {{ template "kubectl.image" . }}
command:
- /bin/bash
- /scripts/install.sh
@@ -34,7 +64,7 @@ spec:
volumeMounts:
- mountPath: /scripts
name: scripts
resources: {{- toYaml .Values.preUpgrade.resources | nindent 12 }}
resources: {{- toYaml .Values.kubectl.resources | nindent 12 }}
volumes:
- name: scripts
configMap:

View File

@@ -1,12 +1,11 @@
# Default values for ks-crds.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
preUpgrade:
kubectl:
image:
registry: ""
repository: kubesphereio/kubectl
tag: "v1.27.12"
repository: kubesphere/kubectl
tag: "v1.27.16"
pullPolicy: IfNotPresent
resources:
limits:
@@ -14,4 +13,4 @@ preUpgrade:
memory: 1024Mi
requests:
cpu: 20m
memory: 100Mi
memory: 100Mi

View File

@@ -2,55 +2,9 @@
# set -x
CRD_NAMES=$1
MAPPING_CONFIG=$2
for extension in `kubectl get installplan -o json | jq -r '.items[] | select(.status.state == "Installed") | .metadata.name'`
do
namespace=$(kubectl get installplan $extension -o=jsonpath='{.status.targetNamespace}')
version=$(kubectl get extension $extension -o=jsonpath='{.status.installedVersion}')
extensionversion=$extension-$version
echo "Found extension $extensionversion installed"
helm status $extension --namespace $namespace
if [ $? -eq 0 ]; then
helm mapkubeapis $extension --namespace $namespace --mapfile $MAPPING_CONFIG
fi
helm status $extension-agent --namespace $namespace
if [ $? -eq 0 ]; then
helm mapkubeapis $extension-agent --namespace $namespace --mapfile $MAPPING_CONFIG
fi
done
# remove namespace's finalizers && ownerReferences
kubectl patch workspaces.tenant.kubesphere.io system-workspace -p '{"metadata":{"finalizers":[]}}' --type=merge
kubectl patch workspacetemplates.tenant.kubesphere.io system-workspace -p '{"metadata":{"finalizers":[]}}' --type=merge
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}' -l 'kubesphere.io/managed=true')
do
kubectl label ns $ns kubesphere.io/workspace- && \
kubectl patch ns $ns -p '{"metadata":{"ownerReferences":[]}}' --type=merge && \
echo "{\"kind\":\"Namespace\",\"apiVersion\":\"v1\",\"metadata\":{\"name\":\"$ns\",\"finalizers\":null}}" | kubectl replace --raw "/api/v1/namespaces/$ns/finalize" -f -
done
# delete crds
for crd in `kubectl get crds -o jsonpath="{.items[*].metadata.name}"`
do
if [[ ${CRD_NAMES[@]/${crd}/} != ${CRD_NAMES[@]} ]]; then
scop=$(eval echo $(kubectl get crd ${crd} -o jsonpath="{.spec.scope}"))
if [[ $scop =~ "Namespaced" ]] ; then
kubectl get $crd -A --no-headers | awk '{print $1" "$2" ""'$crd'"}' | xargs -n 3 sh -c 'kubectl patch $2 -n $0 $1 -p "{\"metadata\":{\"finalizers\":null}}" --type=merge 2>/dev/null && kubectl delete $2 -n $0 $1 2>/dev/null'
else
kubectl get $crd -A --no-headers | awk '{print $1" ""'$crd'"}' | xargs -n 2 sh -c 'kubectl patch $1 $0 -p "{\"metadata\":{\"finalizers\":null}}" --type=merge 2>/dev/null && kubectl delete $1 $0 2>/dev/null'
fi
kubectl delete crd $crd 2>/dev/null;
fi
done
EXTENSION_RELATED_RESOURCES='jobs.batch roles.rbac.authorization.k8s.io rolebindings.rbac.authorization.k8s.io clusterroles.rbac.authorization.k8s.io clusterrolebindings.rbac.authorization.k8s.io'
for resource in $EXTENSION_RELATED_RESOURCES;do
echo "kubectl delete $resource -l kubesphere.io/extension-ref --all-namespaces"
kubectl delete $resource -l kubesphere.io/managed=true --all-namespaces
done
done

View File

@@ -33,8 +33,8 @@ Return the proper image name
{{ include "common.images.image" (dict "imageRoot" .Values.redis.image "global" .Values.global) }}
{{- end -}}
{{- define "preUpgrade.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.preUpgrade.image "global" .Values.global) }}
{{- define "extensions_museum.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.ksExtensionRepository.image "global" .Values.global) }}
{{- end -}}
{{- define "common.images.image" -}}
@@ -70,6 +70,10 @@ Return the proper Docker Image Registry Secret Names
{{- include "common.images.pullSecrets" (dict "images" (list .Values.controller.image) "global" .Values.global) -}}
{{- end -}}
{{- define "extensions_museum.imagePullSecrets" -}}
{{- include "common.images.pullSecrets" (dict "images" (list .Values.ksExtensionRepository.image) "global" .Values.global) -}}
{{- end -}}
{{- define "common.images.pullSecrets" -}}
{{- $pullSecrets := list }}

View File

@@ -0,0 +1,109 @@
{{- if .Values.ksExtensionRepository.enabled }}
{{- $ca := genCA "self-signed-ca" 3650 }}
{{- $cn := printf "%s-extensions-museum" .Release.Name }}
{{- $altName1 := printf "extensions-museum.%s" .Release.Namespace }}
{{- $altName2 := printf "extensions-museum.%s.svc" .Release.Namespace }}
{{- $cert := genSignedCert $cn nil (list $altName1 $altName2) 3650 $ca }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: extensions-museum
namespace: {{ .Release.Namespace }}
labels:
app: extensions-museum
spec:
replicas: 1
selector:
matchLabels:
app: extensions-museum
template:
metadata:
labels:
app: extensions-museum
annotations:
# force restart ks-apiserver after the upgrade is complete if kubesphere-config changes
checksum/cert: {{ sha256sum $cert.Cert }}
spec:
{{- include "extensions_museum.imagePullSecrets" . | nindent 6 }}
containers:
- name: extensions-museum
image: {{ template "extensions_museum.image" . }}
command:
- "/chartmuseum"
- "--storage-local-rootdir"
- "/charts"
- "--storage"
- "local"
- "--tls-cert"
- "/etc/certs/tls.crt"
- "--tls-key"
- "/etc/certs/tls.key"
ports:
- containerPort: 8080
volumeMounts:
- name: certs
mountPath: /etc/certs/
volumes:
- name: certs
secret:
secretName: extensions-museum-certs
---
apiVersion: v1
kind: Secret
metadata:
name: extensions-museum-certs
namespace: {{ .Release.Namespace }}
type: kubernetes.io/tls
data:
ca.crt: {{ b64enc $ca.Cert }}
tls.crt: {{ b64enc $cert.Cert }}
tls.key: {{ b64enc $cert.Key }}
---
apiVersion: v1
kind: Service
metadata:
name: extensions-museum
namespace: {{ .Release.Namespace }}
spec:
selector:
app: extensions-museum
ports:
- protocol: TCP
port: 443
targetPort: 8080
---
apiVersion: kubesphere.io/v1alpha1
kind: Repository
metadata:
name: extensions-museum
spec:
url: https://extensions-museum.{{ .Release.Namespace }}.svc
caBundle: {{ b64enc $ca.Cert }}
---
apiVersion: {{ if semverCompare ">=1.20.0" .Capabilities.KubeVersion.Version }}batch/v1{{ else }}batch/v1beta1{{end}}
kind: CronJob
metadata:
name: restart-extensions-museum
namespace: {{ .Release.Namespace }}
spec:
schedule: "0 0 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: restart-extensions-museum
image: {{ template "kubectl.image" . }}
command:
- /bin/sh
- -c
- "kubectl rollout restart deployment/extensions-museum -n {{ .Release.Namespace }}"
restartPolicy: OnFailure
serviceAccountName: {{ template "ks-core.serviceAccountName" . }}
{{end}}

View File

@@ -69,15 +69,9 @@ data:
appSelector: {{ .Values.composedApp.appSelector | quote }}
kubesphere:
tls: {{ .Values.internalTLS }}
{{- if and .Values.cloud.enabled (eq (include "role" .) "host") }}
{{- if and .Values.telemetry.enabled (eq (include "role" .) "host") }}
telemetry:
{{- if eq .Values.cloud.env "clouddev.kubesphere.io" }}
ksCloudURL: "https://clouddev.kubesphere.io"
{{- else if eq .Values.cloud.env "kubesphere.cloud" }}
ksCloudURL: "https://kubesphere.cloud"
{{- else if and .Values.cloud.customEnv .Values.cloud.customEnv.url }}
ksCloudURL: {{ $.Values.cloud.customEnv.url | quote }}
{{- end }}
{{- end }}
{{- if .Values.ha.enabled -}}
{{- if .Values.ha.cache }}

View File

@@ -1,4 +1,3 @@
{{- $kubeVersion := .Capabilities.KubeVersion }}
apiVersion: v1
kind: ConfigMap
metadata:
@@ -8,15 +7,6 @@ metadata:
"helm.sh/hook-weight": "-1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed
data:
map.yaml: |
mappings:
{{- range $path, $_ := .Files.Glob "charts/ks-crds/crds/**" }}
{{- $crd := $.Files.Get $path | fromYaml }}
{{- range $_, $version := $crd.spec.versions }}
- deprecatedAPI: "apiVersion: {{ $crd.spec.group }}/{{ $version.name }}\nkind: {{ $crd.spec.names.kind }}\n"
removedInVersion: "{{ $kubeVersion }}"
{{- end }}
{{- end }}
{{ (.Files.Glob "scripts/post-delete.sh").AsConfig | indent 2 }}
---
@@ -51,12 +41,6 @@ subjects:
---
{{- $crdNameList := list }}
{{- range $path, $_ := .Files.Glob "charts/ks-crds/crds/**" }}
{{- $crd := $.Files.Get $path | fromYaml }}
{{- $crdNameList = append $crdNameList $crd.metadata.name }}
{{- end }}
apiVersion: batch/v1
kind: Job
metadata:
@@ -76,8 +60,6 @@ spec:
command:
- /bin/bash
- /scripts/post-delete.sh
- '{{ join " " $crdNameList }}'
- /scripts/map.yaml
volumeMounts:
- mountPath: /scripts
name: scripts

View File

@@ -2,7 +2,7 @@
## @param global.tag Global Docker image tag
global:
imageRegistry: docker.io
tag: v4.1.1
tag: v4.1.2
imagePullSecrets: []
## @param nameOverride String to partially override common.names.fullname
@@ -320,7 +320,10 @@ nodeShell:
tag: "v1.27.16"
pullPolicy: IfNotPresent
cloud:
# Telemetry collects aggregated information about the versions of KubeSphere, Kubernetes, and the extensions used.
# KubeSphere Cloud uses this information to help improve the product and does not share it with third-parties.
# If you prefer not to share this data, you can keep this setting disabled.
telemetry:
enabled: true
extension:
@@ -418,7 +421,7 @@ redisHA:
- ""
ksCRDs:
preUpgrade:
kubectl:
image:
registry: ""
repository: kubesphere/kubectl
@@ -431,3 +434,12 @@ ksCRDs:
requests:
cpu: 20m
memory: 100Mi
# add museum for all ks-extensions
ksExtensionRepository:
enabled: true
image:
registry: ""
repository: kubesphere/ks-extensions-museum
tag: "latest"
pullPolicy: Always

View File

@@ -17,6 +17,6 @@ for PKG in "${PKGS[@]}"; do
go run ./vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go object:headerFile=./hack/boilerplate.go.txt paths=./staging/src/kubesphere.io/api/"${PKG}"
else
echo "Generating manifests for ${PKG}"
go run ./vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go object:headerFile=./hack/boilerplate.go.txt paths=./staging/src/kubesphere.io/api/"${PKG}" rbac:roleName=controller-perms "${CRD_OPTIONS}" output:crd:artifacts:config=config/ks-core/crds
go run ./vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go object:headerFile=./hack/boilerplate.go.txt paths=./staging/src/kubesphere.io/api/"${PKG}" rbac:roleName=controller-perms "${CRD_OPTIONS}" output:crd:artifacts:config=config/ks-core/charts/ks-crds/crds
fi
done

View File

@@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
"k8s.io/klog/v2"
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
"kubesphere.io/api/iam/v1beta1"
@@ -398,6 +399,9 @@ func (a *auditing) eventToBytes(events []*Event) [][]byte {
return res
}
var _ http.ResponseWriter = &ResponseCapture{}
var _ responsewriter.UserProvidedDecorator = &ResponseCapture{}
type ResponseCapture struct {
http.ResponseWriter
wroteHeader bool
@@ -413,6 +417,10 @@ func NewResponseCapture(w http.ResponseWriter) *ResponseCapture {
}
}
func (c *ResponseCapture) Unwrap() http.ResponseWriter {
return c.ResponseWriter
}
func (c *ResponseCapture) Header() http.Header {
return c.ResponseWriter.Header()
}
@@ -425,9 +433,6 @@ func (c *ResponseCapture) Write(data []byte) (int, error) {
if err != nil {
return n, err
}
if flusher, ok := c.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
return n, nil
}

View File

@@ -8,6 +8,7 @@ package filters
import (
"net/http"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
"k8s.io/klog/v2"
"kubesphere.io/kubesphere/pkg/apiserver/auditing"
@@ -44,7 +45,7 @@ func (a *auditingFilter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if event := a.LogRequestObject(req, info); event != nil {
resp := auditing.NewResponseCapture(w)
a.next.ServeHTTP(resp, req)
a.next.ServeHTTP(responsewriter.WrapForHTTP1Or2(resp), req)
go a.LogResponseObject(event, resp)
} else {
a.next.ServeHTTP(w, req)

View File

@@ -13,6 +13,7 @@ import (
"strconv"
"time"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
"k8s.io/klog/v2"
"kubesphere.io/kubesphere/pkg/apiserver/metrics"
@@ -20,12 +21,19 @@ import (
"kubesphere.io/kubesphere/pkg/utils/iputil"
)
var _ http.ResponseWriter = &metaResponseWriter{}
var _ responsewriter.UserProvidedDecorator = &metaResponseWriter{}
type metaResponseWriter struct {
http.ResponseWriter
statusCode int
size int
}
func (r *metaResponseWriter) Unwrap() http.ResponseWriter {
return r.ResponseWriter
}
func newMetaResponseWriter(w http.ResponseWriter) *metaResponseWriter {
return &metaResponseWriter{
ResponseWriter: w,
@@ -48,9 +56,6 @@ func (r *metaResponseWriter) Write(b []byte) (int, error) {
if err != nil {
return size, err
}
if flusher, ok := r.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
return size, nil
}
@@ -66,7 +71,8 @@ func WithGlobalFilter(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
wrapper := newMetaResponseWriter(w)
start := time.Now()
handler.ServeHTTP(wrapper, req)
handler.ServeHTTP(responsewriter.WrapForHTTP1Or2(wrapper), req)
elapsedTime := time.Since(start)
// Record metrics for each request
@@ -94,7 +100,7 @@ func WithGlobalFilter(handler http.Handler) http.Handler {
req.Proto,
wrapper.statusCode,
wrapper.size,
elapsedTime.Microseconds(),
elapsedTime.Milliseconds(),
)
})
}

View File

@@ -157,8 +157,7 @@ func (h *Helper) AggregationRole(ctx context.Context, ruleOwner RuleOwner, recor
if !cover {
needUpdate = true
newRule := append(ruleOwner.GetRules(), uncovered...)
squashedRules := SquashRules(len(newRule), newRule)
ruleOwner.SetRules(squashedRules)
ruleOwner.SetRules(newRule)
}
if !templateNamesEqual {

View File

@@ -149,7 +149,8 @@ func ruleCovers(ownerRule, subRule rbacv1.PolicyRule) bool {
verbMatches := has(ownerRule.Verbs, rbacv1.VerbAll) || hasAll(ownerRule.Verbs, subRule.Verbs)
groupMatches := has(ownerRule.APIGroups, rbacv1.APIGroupAll) || hasAll(ownerRule.APIGroups, subRule.APIGroups)
resourceMatches := resourceCoversAll(ownerRule.Resources, subRule.Resources)
nonResourceURLMatches := nonResourceURLsCoversAll(ownerRule.NonResourceURLs, subRule.NonResourceURLs)
nonResourceURLMatches := (len(ownerRule.NonResourceURLs) == 0 && len(subRule.NonResourceURLs) == 0) || (len(ownerRule.Resources) == 0 &&
len(subRule.Resources) == 0 && nonResourceURLsCoversAll(ownerRule.NonResourceURLs, subRule.NonResourceURLs))
resourceNameMatches := false

View File

@@ -265,12 +265,12 @@ func (r *AppReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request)
if strings.Contains(release.Info.Description, "context deadline exceeded") && reCheck < timeoutMaxRecheck {
if apprls.Status.State != appv2.StatusTimeout {
err = r.updateStatus(ctx, apprls, appv2.StatusTimeout, "install time out")
err = r.updateStatus(ctx, apprls, appv2.StatusTimeout, "Installation timeout")
if err != nil {
klog.Errorf("failed to update apprelease %s status : %v", apprls.Name, err)
return ctrl.Result{}, err
}
klog.Infof("install time out, will check status again after %d second", timeoutVerificationAgain)
klog.Infof("Installation timeout, will check status again after %d second", timeoutVerificationAgain)
return ctrl.Result{RequeueAfter: timeoutVerificationAgain * time.Second}, nil
}

View File

@@ -247,7 +247,7 @@ func repoParseRequest(cli client.Client, versions helmrepo.ChartVersions, helmRe
FromRepo: true,
}
url := ver.URLs[0]
methodList := []string{"https://", "http://", "s3://"}
methodList := []string{"https://", "http://", "s3://", "oci://"}
needContact := true
for _, method := range methodList {
if strings.HasPrefix(url, method) {

View File

@@ -456,26 +456,40 @@ func (r *InstallPlanReconciler) loadChartData(ctx context.Context) ([]byte, stri
switch chartURL.Scheme {
case registry.OCIScheme:
opts := make([]getter.Option, 0)
opts = append(opts, getter.WithInsecureSkipVerifyTLS(true))
if extensionVersion.Spec.Repository != "" {
opts = append(opts, getter.WithInsecureSkipVerifyTLS(repo.Spec.Insecure))
}
if repo.Spec.BasicAuth != nil {
opts = append(opts, getter.WithBasicAuth(repo.Spec.BasicAuth.Username, repo.Spec.BasicAuth.Password))
}
chartGetter, err = getter.NewOCIGetter(opts...)
if err != nil {
return nil, "", fmt.Errorf("failed to create chart getter: %v", err)
}
case "http", "https":
opts := make([]getter.Option, 0)
if chartURL.Scheme == "https" && extensionVersion.Spec.Repository != "" {
opts = append(opts, getter.WithInsecureSkipVerifyTLS(repo.Spec.Insecure))
}
if repo.Spec.CABundle != "" {
caFile, err := storeCAFile(repo.Spec.CABundle, repo.Name)
if err != nil {
return nil, "", fmt.Errorf("failed to store CABundle to local file: %s", err)
}
opts = append(opts, getter.WithTLSClientConfig("", "", caFile))
}
if chartURL.Scheme == "https" {
opts = append(opts, getter.WithInsecureSkipVerifyTLS(true))
opts = append(opts, getter.WithInsecureSkipVerifyTLS(repo.Spec.Insecure))
}
if repo.Spec.BasicAuth != nil {
opts = append(opts, getter.WithBasicAuth(repo.Spec.BasicAuth.Username, repo.Spec.BasicAuth.Password))
}
chartGetter, err = getter.NewHTTPGetter(opts...)
if err != nil {
return nil, "", fmt.Errorf("failed to create chart getter: %v", err)
}
default:
err = fmt.Errorf("unsupported scheme: %s", chartURL.Scheme)
}
if err != nil {
return nil, "", fmt.Errorf("failed to create chart getter: %v", err)
return nil, "", fmt.Errorf("unsupported scheme: %s", chartURL.Scheme)
}
buffer, err := chartGetter.Get(chartURL.String())

View File

@@ -11,7 +11,6 @@ import (
"encoding/base64"
"errors"
"fmt"
"io"
"mime"
"net/http"
"net/url"
@@ -19,10 +18,6 @@ import (
"strings"
"time"
kscontroller "kubesphere.io/kubesphere/pkg/controller"
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
"github.com/go-logr/logr"
"helm.sh/helm/v3/pkg/chart/loader"
appsv1 "k8s.io/api/apps/v1"
@@ -35,6 +30,7 @@ import (
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
"kubesphere.io/utils/helm"
ctrl "sigs.k8s.io/controller-runtime"
@@ -43,6 +39,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"kubesphere.io/kubesphere/pkg/constants"
kscontroller "kubesphere.io/kubesphere/pkg/controller"
)
const (
@@ -52,6 +49,8 @@ const (
defaultRequeueInterval = 15 * time.Second
generateNameFormat = "repository-%s"
extensionFileName = "extension.yaml"
// caTemplate store repository.spec.caBound in local dir.
caTemplate = "{{ .TempDIR }}/repository/{{ .RepositoryName }}/ssl/ca.crt"
)
var extensionRepoConflict = fmt.Errorf("extension repo mismatch")
@@ -178,10 +177,10 @@ func (r *RepositoryReconciler) syncExtensionsFromURL(ctx context.Context, repo *
logger := klog.FromContext(ctx)
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
cred := helm.RepoCredential{}
if repo.Spec.BasicAuth != nil {
cred.Username = repo.Spec.BasicAuth.Username
cred.Password = repo.Spec.BasicAuth.Password
cred, err := newHelmCred(repo)
if err != nil {
return err
}
index, err := helm.LoadRepoIndex(ctx, repoURL, cred)
if err != nil {
@@ -216,7 +215,7 @@ func (r *RepositoryReconciler) syncExtensionsFromURL(ctx context.Context, repo *
}
}
extensionVersionSpec, err := r.loadExtensionVersionSpecFrom(ctx, chartURL, repo)
extensionVersionSpec, err := r.loadExtensionVersionSpecFrom(ctx, chartURL, repo, cred)
if err != nil {
return fmt.Errorf("failed to load extension version spec: %s", err)
}
@@ -435,37 +434,19 @@ func (r *RepositoryReconciler) deployRepository(ctx context.Context, repo *corev
return nil
}
func (r *RepositoryReconciler) loadExtensionVersionSpecFrom(ctx context.Context, chartURL string, repo *corev1alpha1.Repository) (*corev1alpha1.ExtensionVersionSpec, error) {
func (r *RepositoryReconciler) loadExtensionVersionSpecFrom(ctx context.Context, chartURL string, repo *corev1alpha1.Repository, cred helm.RepoCredential) (*corev1alpha1.ExtensionVersionSpec, error) {
logger := klog.FromContext(ctx)
var result *corev1alpha1.ExtensionVersionSpec
err := retry.OnError(retry.DefaultRetry, func(err error) bool {
return true
}, func() error {
req, err := http.NewRequest(http.MethodGet, chartURL, nil)
data, err := helm.LoadData(ctx, chartURL, cred)
if err != nil {
return err
}
if repo.Spec.BasicAuth != nil {
req.SetBasicAuth(repo.Spec.BasicAuth.Username, repo.Spec.BasicAuth.Password)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf(string(data))
}
files, err := loader.LoadArchiveFiles(resp.Body)
files, err := loader.LoadArchiveFiles(data)
if err != nil {
return err
}

View File

@@ -7,15 +7,18 @@ package core
import (
"bytes"
"encoding/base64"
goerrors "errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
yaml3 "gopkg.in/yaml.v3"
"text/template"
"github.com/Masterminds/semver/v3"
yaml3 "gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/storage/driver"
rbacv1 "k8s.io/api/rbac/v1"
@@ -23,9 +26,9 @@ import (
"k8s.io/klog/v2"
clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
"kubesphere.io/utils/helm"
"kubesphere.io/kubesphere/pkg/utils/hashutil"
"kubesphere.io/kubesphere/pkg/version"
)
@@ -264,3 +267,58 @@ func configChanged(sub *corev1alpha1.InstallPlan, cluster string) bool {
}
return newConfigHash != oldConfigHash
}
// newHelmCred from Repository
func newHelmCred(repo *corev1alpha1.Repository) (helm.RepoCredential, error) {
cred := helm.RepoCredential{
InsecureSkipTLSVerify: repo.Spec.Insecure,
}
if repo.Spec.CABundle != "" {
caFile, err := storeCAFile(repo.Spec.CABundle, repo.Name)
if err != nil {
return cred, err
}
cred.CAFile = caFile
}
if repo.Spec.BasicAuth != nil {
cred.Username = repo.Spec.BasicAuth.Username
cred.Password = repo.Spec.BasicAuth.Password
}
return cred, nil
}
// storeCAFile in local file from caTemplate.
func storeCAFile(caBundle string, repoName string) (string, error) {
var buff = &bytes.Buffer{}
tmpl, err := template.New("repositoryCABundle").Parse(caTemplate)
if err != nil {
return "", err
}
if err := tmpl.Execute(buff, map[string]string{
"TempDIR": os.TempDir(),
"RepositoryName": repoName,
}); err != nil {
return "", err
}
caFile := buff.String()
if _, err := os.Stat(filepath.Dir(caFile)); err != nil {
if !os.IsNotExist(err) {
return "", err
}
if err := os.MkdirAll(filepath.Dir(caFile), os.ModePerm); err != nil {
return "", err
}
}
data, err := base64.StdEncoding.DecodeString(caBundle)
if err != nil {
return "", err
}
if err := os.WriteFile(caFile, data, os.ModePerm); err != nil {
return "", err
}
return caFile, nil
}

View File

@@ -140,7 +140,7 @@ func (r *Reconciler) reconcileWorkspaceOwnerReference(ctx context.Context, names
return nil
}
if !metav1.IsControlledBy(namespace, workspace) {
if !metav1.IsControlledBy(namespace, workspace) && namespace.Labels[constants.KubeSphereManagedLabel] == "true" {
namespace = namespace.DeepCopy()
if err := controllerutil.SetControllerReference(workspace, namespace, scheme.Scheme); err != nil {
return err

View File

@@ -42,8 +42,11 @@ var _ = Describe("Namespace", func() {
It("Should create successfully", func() {
namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-namespace",
Labels: map[string]string{tenantv1beta1.WorkspaceLabel: workspace.Name},
Name: "test-namespace",
Labels: map[string]string{
tenantv1beta1.WorkspaceLabel: workspace.Name,
constants.KubeSphereManagedLabel: "true",
},
},
}

View File

@@ -40,19 +40,19 @@ func (c *Controller) WatchOpenAPIChanges(ctx context.Context, cache runtimecache
AddFunc: func(obj interface{}) {
apiService := obj.(*extensionsv1alpha1.APIService)
if err := openAPIV2Service.AddUpdateApiService(apiService); err != nil {
klog.Error(err)
klog.V(4).Infof("add openapi v2 service failed: %v", err)
}
if err := openAPIV3Service.AddUpdateApiService(apiService); err != nil {
klog.Error(err)
klog.V(4).Infof("add openapi v3 service failed: %v", err)
}
},
UpdateFunc: func(old, new interface{}) {
apiService := new.(*extensionsv1alpha1.APIService)
if err := openAPIV2Service.AddUpdateApiService(apiService); err != nil {
klog.Error(err)
klog.V(4).Infof("update openapi v2 service failed: %v", err)
}
if err := openAPIV3Service.AddUpdateApiService(apiService); err != nil {
klog.Error(err)
klog.V(4).Infof("update openapi v3 service failed: %v", err)
}
},
DeleteFunc: func(obj interface{}) {

View File

@@ -96,9 +96,9 @@ func (r *Reconciler) syncToKubernetes(ctx context.Context, role *iamv1beta1.Role
})
if err != nil {
r.logger.Error(err, "sync role failed", "role", role.Name)
r.logger.Error(err, "sync role failed", "namespace", role.Namespace, "role", role.Name)
}
r.logger.V(4).Info("sync role to K8s", "role", role.Name, "op", op)
r.logger.V(4).Info("sync role to K8s", "namespace", role.Namespace, "role", role.Name, "op", op)
return nil
}

View File

@@ -7,8 +7,12 @@ package v1alpha1
import (
"bytes"
"encoding/base64"
"fmt"
"net/url"
"os"
"path/filepath"
"text/template"
"github.com/emicklei/go-restful/v3"
"helm.sh/helm/v3/pkg/chart/loader"
@@ -22,6 +26,8 @@ import (
"kubesphere.io/kubesphere/pkg/api"
)
var caTemplate = "{{ .TempDIR }}/repository/{{ .RepositoryName }}/ssl/ca.crt"
type handler struct {
cache runtimeclient.Reader
}
@@ -71,23 +77,40 @@ func (h *handler) ListFiles(request *restful.Request, response *restful.Response
switch chartURL.Scheme {
case registry.OCIScheme:
opts := make([]getter.Option, 0)
opts = append(opts, getter.WithInsecureSkipVerifyTLS(true))
if extensionVersion.Spec.Repository != "" {
opts = append(opts, getter.WithInsecureSkipVerifyTLS(repo.Spec.Insecure))
}
if repo.Spec.BasicAuth != nil {
opts = append(opts, getter.WithBasicAuth(repo.Spec.BasicAuth.Username, repo.Spec.BasicAuth.Password))
}
chartGetter, err = getter.NewOCIGetter(opts...)
if err != nil {
api.HandleInternalError(response, request, fmt.Errorf("failed to create chart getter: %v", err))
return
}
case "http", "https":
options := make([]getter.Option, 0)
if chartURL.Scheme == "https" {
options = append(options, getter.WithInsecureSkipVerifyTLS(true))
opts := make([]getter.Option, 0)
if chartURL.Scheme == "https" && extensionVersion.Spec.Repository != "" {
opts = append(opts, getter.WithInsecureSkipVerifyTLS(repo.Spec.Insecure))
}
if repo.Spec.CABundle != "" {
caFile, err := storeCAFile(repo.Spec.CABundle, repo.Name)
if err != nil {
api.HandleInternalError(response, request, fmt.Errorf("failed to store CABundle to local file: %s", err))
return
}
opts = append(opts, getter.WithTLSClientConfig("", "", caFile))
}
if repo.Spec.BasicAuth != nil {
options = append(options, getter.WithBasicAuth(repo.Spec.BasicAuth.Username, repo.Spec.BasicAuth.Password))
opts = append(opts, getter.WithBasicAuth(repo.Spec.BasicAuth.Username, repo.Spec.BasicAuth.Password))
}
chartGetter, err = getter.NewHTTPGetter(options...)
}
if err != nil {
api.HandleInternalError(response, request, fmt.Errorf("failed to create chart getter: %v", err))
chartGetter, err = getter.NewHTTPGetter(opts...)
if err != nil {
api.HandleInternalError(response, request, fmt.Errorf("failed to create chart getter: %v", err))
return
}
default:
api.HandleInternalError(response, request, fmt.Errorf("cannot support chartURL %s, it's schame should be: oci,http,https", extensionVersion.Spec.ChartURL))
return
}
@@ -105,3 +128,39 @@ func (h *handler) ListFiles(request *restful.Request, response *restful.Response
_ = response.WriteEntity(files)
}
// storeCAFile in local file from caTemplate.
func storeCAFile(caBundle string, repoName string) (string, error) {
var buff = &bytes.Buffer{}
tmpl, err := template.New("repositoryCABundle").Parse(caTemplate)
if err != nil {
return "", err
}
if err := tmpl.Execute(buff, map[string]string{
"TempDIR": os.TempDir(),
"RepositoryName": repoName,
}); err != nil {
return "", err
}
caFile := buff.String()
if _, err := os.Stat(filepath.Dir(caFile)); err != nil {
if !os.IsNotExist(err) {
return "", err
}
if err := os.MkdirAll(filepath.Dir(caFile), os.ModePerm); err != nil {
return "", err
}
}
data, err := base64.StdEncoding.DecodeString(caBundle)
if err != nil {
return "", err
}
if err := os.WriteFile(caFile, data, os.ModePerm); err != nil {
return "", err
}
return caFile, nil
}

View File

@@ -29,6 +29,7 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/registry"
helmrelease "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
@@ -340,6 +341,10 @@ func HelmPull(u string, cred appv2.RepoCredential) (*bytes.Buffer, error) {
}
func LoadRepoIndex(u string, cred appv2.RepoCredential) (idx helmrepo.IndexFile, err error) {
if registry.IsOCI(u) {
return LoadRepoIndexFromOci(u, cred)
}
if !strings.HasSuffix(u, "/") {
u = fmt.Sprintf("%s/index.yaml", u)
} else {

View File

@@ -0,0 +1,199 @@
package application
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"helm.sh/helm/v3/pkg/registry"
helmrepo "helm.sh/helm/v3/pkg/repo"
"k8s.io/klog/v2"
appv2 "kubesphere.io/api/application/v2"
"kubesphere.io/kubesphere/pkg/simple/client/oci"
)
func HelmPullFromOci(u string, cred appv2.RepoCredential) ([]byte, error) {
if !registry.IsOCI(u) {
return nil, fmt.Errorf("invalid oci URL format: %s", u)
}
_, err := url.Parse(u)
if err != nil {
klog.Errorf("invalid oci chart URL format: %s, err:%v", u, err)
return nil, err
}
client, err := newOCIRegistryClient(u, cred)
if err != nil {
return nil, err
}
pullRef := strings.TrimPrefix(u, fmt.Sprintf("%s://", registry.OCIScheme))
pullResult, err := client.Pull(pullRef)
if err != nil {
klog.Errorf("An error occurred to pull chart from repository: %s,err:%v", pullRef, err)
return nil, err
}
return pullResult.Chart.Data, nil
}
func LoadRepoIndexFromOci(u string, cred appv2.RepoCredential) (idx helmrepo.IndexFile, err error) {
if !registry.IsOCI(u) {
return idx, fmt.Errorf("invalid oci URL format: %s", u)
}
parsedURL, err := url.Parse(u)
if err != nil {
klog.Errorf("invalid repo URL format: %s, err:%v", u, err)
return idx, err
}
repoCharts, err := GetRepoChartsFromOci(parsedURL, cred)
if err != nil {
return idx, err
}
if len(repoCharts) == 0 {
return idx, nil
}
client, err := newOCIRegistryClient(u, cred)
if err != nil {
return idx, err
}
index := helmrepo.NewIndexFile()
for _, repoChart := range repoCharts {
tags, err := client.Tags(fmt.Sprintf("%s/%s", parsedURL.Host, repoChart))
if err != nil {
klog.Errorf("An error occurred to load tags from repository: %s/%s,err:%v", parsedURL.Host, repoChart, err)
continue
}
if len(tags) == 0 {
klog.Errorf("Unable to locate any tags in provided repository: %s/%s,err:%v", parsedURL.Host, repoChart, err)
continue
}
for _, tag := range tags {
pullRef := fmt.Sprintf("%s/%s:%s", parsedURL.Host, repoChart, tag)
pullResult, err := client.Pull(pullRef)
if err != nil {
klog.Errorf("An error occurred to pull chart from repository: %s,err:%v", pullRef, err)
continue
}
baseUrl := fmt.Sprintf("%s://%s", registry.OCIScheme, pullRef)
hash := strings.TrimPrefix(pullResult.Chart.Digest, "sha256:")
if err := index.MustAdd(pullResult.Chart.Meta, "", baseUrl, hash); err != nil {
klog.Errorf("failed adding chart metadata to index with repository: %s,err:%v", pullRef, err)
continue
}
}
}
index.SortEntries()
return *index, nil
}
func GetRepoChartsFromOci(parsedURL *url.URL, cred appv2.RepoCredential) ([]string, error) {
if parsedURL == nil {
return nil, errors.New("missing parsedURL")
}
skipTLS := true
if cred.InsecureSkipTLSVerify != nil && !*cred.InsecureSkipTLSVerify {
skipTLS = false
}
reg, err := oci.NewRegistry(parsedURL.Host,
oci.WithTimeout(5*time.Second),
oci.WithBasicAuth(cred.Username, cred.Password),
oci.WithInsecureSkipVerifyTLS(skipTLS))
if err != nil {
return nil, err
}
ctx := context.Background()
repoPath := strings.TrimSuffix(parsedURL.Path, "/")
repoPath = strings.TrimPrefix(repoPath, "/")
var repoCharts []string
err = reg.Repositories(ctx, "", func(repos []string) error {
cutPrefix := repoPath
if cutPrefix != "" {
cutPrefix = cutPrefix + "/"
}
for _, repo := range repos {
if subRepo, found := strings.CutPrefix(repo, cutPrefix); found && subRepo != "" {
if !strings.Contains(subRepo, "/") {
repoCharts = append(repoCharts, fmt.Sprintf("%s/%s", repoPath, subRepo))
}
}
}
return nil
})
if err != nil {
return nil, err
}
return repoCharts, nil
}
func newOCIRegistryClient(u string, cred appv2.RepoCredential) (*registry.Client, error) {
parsedURL, err := url.Parse(u)
if err != nil {
klog.Errorf("invalid oci repo URL format: %s, err:%v", u, err)
return nil, err
}
skipTLS := true
if cred.InsecureSkipTLSVerify != nil && !*cred.InsecureSkipTLSVerify {
skipTLS = false
}
reg, err := oci.NewRegistry(parsedURL.Host,
oci.WithTimeout(5*time.Second),
oci.WithBasicAuth(cred.Username, cred.Password),
oci.WithInsecureSkipVerifyTLS(skipTLS))
if err != nil {
return nil, err
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLS},
Proxy: http.ProxyFromEnvironment,
}
opts := []registry.ClientOption{registry.ClientOptHTTPClient(&http.Client{
Transport: transport,
Timeout: 5 * time.Second,
})}
if reg.PlainHTTP {
opts = append(opts, registry.ClientOptPlainHTTP())
}
client, err := registry.NewClient(opts...)
if err != nil {
return nil, err
}
if cred.Username != "" || cred.Password != "" {
err = client.Login(parsedURL.Host,
registry.LoginOptBasicAuth(cred.Username, cred.Password),
registry.LoginOptInsecure(reg.PlainHTTP))
if err != nil {
return nil, err
}
}
return client, nil
}

View File

@@ -0,0 +1,65 @@
package application
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
appv2 "kubesphere.io/api/application/v2"
)
func TestLoadRepoIndexFromOci(t *testing.T) {
testRepos := []string{"helmcharts/nginx", "helmcharts/test-api", "helmcharts/test-ui", "helmcharts/demo-app"}
testTags := []string{"1.0.0", "1.2.0", "1.0.3"}
testRepo := testRepos[1]
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && (r.URL.Path == "/v2" || r.URL.Path == "/v2/") {
w.WriteHeader(http.StatusOK)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/v2/_catalog" {
result := struct {
Repositories []string `json:"repositories"`
}{
Repositories: testRepos,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
return
}
if r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf("/v2/%s/tags/list", testRepo) {
result := struct {
Tags []string `json:"tags"`
}{
Tags: testTags,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
return
}
t.Logf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
url := fmt.Sprintf("oci://%s/helmcharts", uri.Host)
cred := appv2.RepoCredential{
Username: "",
Password: "",
}
index, err := LoadRepoIndexFromOci(url, cred)
if err != nil {
t.Errorf("LoadRepoIndexFromOci() error: %s", err)
}
t.Log(len(index.Entries))
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"helm.sh/helm/v3/pkg/registry"
"k8s.io/klog/v2"
"io"
@@ -145,6 +146,9 @@ func DownLoadChart(cli runtimeclient.Client, pullUrl, repoName string) (data []b
klog.Errorf("failed to get app repo, err: %v", err)
return data, err
}
if registry.IsOCI(pullUrl) {
return HelmPullFromOci(pullUrl, repo.Spec.Credential)
}
buf, err := HelmPull(pullUrl, repo.Spec.Credential)
if err != nil {
klog.Errorf("load chart failed, error: %s", err)

View File

@@ -0,0 +1,64 @@
package oci
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"unicode"
)
var maxErrorBytes int64 = 8 * 1024 // 8 KiB
// requestError contains a single error.
type requestError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// Error returns a error string describing the error.
func (e requestError) Error() string {
code := strings.Map(func(r rune) rune {
if r == '_' {
return ' '
}
return unicode.ToLower(r)
}, e.Code)
if e.Message == "" {
return code
}
return fmt.Sprintf("%s: %s", code, e.Message)
}
// requestErrors is a bundle of requestError.
type requestErrors []requestError
// Error returns a error string describing the error.
func (errs requestErrors) Error() string {
switch len(errs) {
case 0:
return "<nil>"
case 1:
return errs[0].Error()
}
var errmsgs []string
for _, err := range errs {
errmsgs = append(errmsgs, err.Error())
}
return strings.Join(errmsgs, "; ")
}
func ParseErrorResponse(resp *http.Response) error {
var errmsg string
var body struct {
Errors requestErrors `json:"errors"`
}
lr := io.LimitReader(resp.Body, maxErrorBytes)
if err := json.NewDecoder(lr).Decode(&body); err == nil && len(body.Errors) > 0 {
errmsg = body.Errors.Error()
} else {
errmsg = http.StatusText(resp.StatusCode)
}
return fmt.Errorf("%s %q: unexpected status code %d: %s", resp.Request.Method, resp.Request.URL, resp.StatusCode, errmsg)
}

View File

@@ -0,0 +1,96 @@
package oci
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"oras.land/oras-go/pkg/registry"
)
func TestRegistry_Api(t *testing.T) {
testRepos := []string{"helmcharts/nginx", "helmcharts/test-api", "helmcharts/test-ui", "helmcharts/demo-app"}
testTags := []string{"1.0.0", "1.2.0", "1.0.3"}
testRepo := testRepos[1]
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && (r.URL.Path == "/v2" || r.URL.Path == "/v2/") {
w.WriteHeader(http.StatusOK)
return
}
if r.Method == http.MethodGet && r.URL.Path == "/v2/_catalog" {
result := struct {
Repositories []string `json:"repositories"`
}{
Repositories: testRepos,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
return
}
if r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf("/v2/%s/tags/list", testRepo) {
result := struct {
Tags []string `json:"tags"`
}{
Tags: testTags,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
return
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
reg, err := NewRegistry(uri.Host,
WithTimeout(5*time.Second),
WithBasicAuth("", ""),
WithInsecureSkipVerifyTLS(true))
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
ctx := context.Background()
err = reg.Ping(ctx)
if err != nil {
t.Fatalf("Registry.Ping() error = %v", err)
}
var registryTags []string
repo, err := reg.Repository(ctx, testRepo)
if err != nil {
t.Fatalf("Registry.Repository() error = %v", err)
}
registryTags, err = registry.Tags(ctx, repo)
if err != nil {
t.Fatalf("Registry.Repository().Tags() error = %v", err)
}
t.Log(len(registryTags))
err = reg.Repositories(ctx, "", func(repos []string) error {
for _, repo := range repos {
if subRepo, found := strings.CutPrefix(repo, ""); found {
t.Log(subRepo)
}
}
return nil
})
if err != nil {
t.Fatalf("Registry.Repositories() error = %v", err)
}
}

View File

@@ -0,0 +1,221 @@
package oci
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"time"
"oras.land/oras-go/pkg/registry"
"oras.land/oras-go/pkg/registry/remote"
"oras.land/oras-go/pkg/registry/remote/auth"
)
type RepositoryOptions remote.Repository
type RegistryOption func(*Registry)
// Registry is an HTTP client to a remote registry by oras-go 2.x.
// Registry with authentication requires an administrator account.
type Registry struct {
RepositoryOptions
RepositoryListPageSize int
username string
password string
timeout time.Duration
insecureSkipVerifyTLS bool
}
func NewRegistry(name string, options ...RegistryOption) (*Registry, error) {
ref := registry.Reference{
Registry: name,
}
if err := ref.ValidateRegistry(); err != nil {
return nil, err
}
reg := &Registry{RepositoryOptions: RepositoryOptions{
Reference: ref,
}}
for _, option := range options {
option(reg)
}
headers := http.Header{}
headers.Set("User-Agent", "kubesphere.io")
reg.Client = &auth.Client{
Client: &http.Client{
Timeout: reg.timeout,
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: reg.insecureSkipVerifyTLS}},
},
Header: headers,
Credential: func(_ context.Context, _ string) (auth.Credential, error) {
if reg.username == "" && reg.password == "" {
return auth.EmptyCredential, nil
}
return auth.Credential{
Username: reg.username,
Password: reg.password,
}, nil
},
}
_, err := reg.IsPlainHttp()
if err != nil {
return nil, err
}
return reg, nil
}
func WithBasicAuth(username, password string) RegistryOption {
return func(reg *Registry) {
reg.username = username
reg.password = password
}
}
func WithTimeout(timeout time.Duration) RegistryOption {
return func(reg *Registry) {
reg.timeout = timeout
}
}
func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) RegistryOption {
return func(reg *Registry) {
reg.insecureSkipVerifyTLS = insecureSkipVerifyTLS
}
}
func (r *Registry) client() remote.Client {
if r.Client == nil {
return auth.DefaultClient
}
return r.Client
}
func (r *Registry) do(req *http.Request) (*http.Response, error) {
return r.client().Do(req)
}
func (r *Registry) IsPlainHttp() (bool, error) {
schemaProbeList := []bool{false, true}
var err error
for _, probe := range schemaProbeList {
r.PlainHTTP = probe
err = r.Ping(context.Background())
if err == nil {
return probe, nil
}
}
return r.PlainHTTP, err
}
func (r *Registry) Ping(ctx context.Context) error {
url := buildRegistryBaseURL(r.PlainHTTP, r.Reference)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := r.do(req)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusNotFound:
return errors.New("not found")
default:
return ParseErrorResponse(resp)
}
}
func (r *Registry) Repositories(ctx context.Context, last string, fn func(repos []string) error) error {
url := buildRegistryCatalogURL(r.PlainHTTP, r.Reference)
var err error
for err == nil {
url, err = r.repositories(ctx, last, fn, url)
// clear `last` for subsequent pages
last = ""
}
if err != errNoLink {
return err
}
return nil
}
func (r *Registry) repositories(ctx context.Context, last string, fn func(repos []string) error, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
if r.RepositoryListPageSize > 0 || last != "" {
q := req.URL.Query()
if r.RepositoryListPageSize > 0 {
q.Set("n", strconv.Itoa(r.RepositoryListPageSize))
}
if last != "" {
q.Set("last", last)
}
req.URL.RawQuery = q.Encode()
}
resp, err := r.do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", ParseErrorResponse(resp)
}
var page struct {
Repositories []string `json:"repositories"`
}
lr := limitReader(resp.Body, r.MaxMetadataBytes)
if err := json.NewDecoder(lr).Decode(&page); err != nil {
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
}
if err := fn(page.Repositories); err != nil {
return "", err
}
return parseLink(resp)
}
func (r *Registry) Repository(ctx context.Context, name string) (registry.Repository, error) {
ref := registry.Reference{
Registry: r.Reference.Registry,
Repository: name,
}
if err := ref.ValidateRepository(); err != nil {
return nil, err
}
repo := r.repository((*remote.Repository)(&r.RepositoryOptions))
repo.Reference = ref
return repo, nil
}
func (r *Registry) repository(repo *remote.Repository) *remote.Repository {
return &remote.Repository{
Client: repo.Client,
Reference: repo.Reference,
PlainHTTP: repo.PlainHTTP,
ManifestMediaTypes: slices.Clone(repo.ManifestMediaTypes),
TagListPageSize: repo.TagListPageSize,
ReferrerListPageSize: repo.ReferrerListPageSize,
MaxMetadataBytes: repo.MaxMetadataBytes,
}
}

View File

@@ -0,0 +1,22 @@
package oci
import (
"fmt"
"oras.land/oras-go/pkg/registry"
)
func buildScheme(plainHTTP bool) string {
if plainHTTP {
return "http"
}
return "https"
}
func buildRegistryBaseURL(plainHTTP bool, ref registry.Reference) string {
return fmt.Sprintf("%s://%s/v2/", buildScheme(plainHTTP), ref.Host())
}
func buildRegistryCatalogURL(plainHTTP bool, ref registry.Reference) string {
return fmt.Sprintf("%s://%s/v2/_catalog", buildScheme(plainHTTP), ref.Host())
}

View File

@@ -0,0 +1,48 @@
package oci
import (
"errors"
"fmt"
"io"
"net/http"
"strings"
)
// defaultMaxMetadataBytes specifies the default limit on how many response
// bytes are allowed in the server's response to the metadata APIs.
// See also: Repository.MaxMetadataBytes
var defaultMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB
// errNoLink is returned by parseLink() when no Link header is present.
var errNoLink = errors.New("no Link header in response")
// parseLink returns the URL of the response's "Link" header, if present.
func parseLink(resp *http.Response) (string, error) {
link := resp.Header.Get("Link")
if link == "" {
return "", errNoLink
}
if link[0] != '<' {
return "", fmt.Errorf("invalid next link %q: missing '<'", link)
}
if i := strings.IndexByte(link, '>'); i == -1 {
return "", fmt.Errorf("invalid next link %q: missing '>'", link)
} else {
link = link[1:i]
}
linkURL, err := resp.Request.URL.Parse(link)
if err != nil {
return "", err
}
return linkURL.String(), nil
}
// limitReader returns a Reader that reads from r but stops with EOF after n
// bytes. If n is zero, defaultMaxMetadataBytes is used.
func limitReader(r io.Reader, n int64) io.Reader {
if n == 0 {
n = defaultMaxMetadataBytes
}
return io.LimitReader(r, n)
}

View File

@@ -18,14 +18,17 @@ type BasicAuth struct {
}
type RepositorySpec struct {
// DEPRECATED: the field will remove in future versions, please use url.
Image string `json:"image,omitempty"`
URL string `json:"url,omitempty"`
Description string `json:"description,omitempty"`
BasicAuth *BasicAuth `json:"basicAuth,omitempty"`
UpdateStrategy *UpdateStrategy `json:"updateStrategy,omitempty"`
// +optional The caBundle (base64 string) is used in helmExecutor to verify the helm server.
// if the caBundle is empty, use --insecure-skip-tls-verify.
// The caBundle (base64 string) is used in helmExecutor to verify the helm server.
// +optional
CABundle string `json:"caBundle,omitempty"`
// --insecure-skip-tls-verify. default false
Insecure bool `json:"insecure,omitempty"`
}
type RepositoryStatus struct {

View File

@@ -24,7 +24,7 @@ func LoadRepoIndex(ctx context.Context, u string, cred RepoCredential) (*helmrep
u = fmt.Sprintf("%s%s", u, IndexYaml)
}
resp, err := loadData(ctx, u, cred)
resp, err := LoadData(ctx, u, cred)
if err != nil {
return nil, err
}
@@ -52,7 +52,7 @@ func loadIndex(data []byte) (*helmrepo.IndexFile, error) {
return i, nil
}
func loadData(ctx context.Context, u string, cred RepoCredential) (*bytes.Buffer, error) {
func LoadData(ctx context.Context, u string, cred RepoCredential) (*bytes.Buffer, error) {
parsedURL, err := url.Parse(u)
if err != nil {
return nil, err
@@ -81,18 +81,11 @@ func loadData(ctx context.Context, u string, cred RepoCredential) (*bytes.Buffer
resp = bytes.NewBuffer(data)
} else {
skipTLS := true
if cred.InsecureSkipTLSVerify != nil && !*cred.InsecureSkipTLSVerify {
skipTLS = false
}
indexURL := parsedURL.String()
// TODO add user-agent
g, _ := getter.NewHTTPGetter()
resp, err = g.Get(indexURL,
resp, err = g.Get(parsedURL.String(),
getter.WithTimeout(5*time.Minute),
getter.WithURL(u),
getter.WithInsecureSkipVerifyTLS(skipTLS),
getter.WithInsecureSkipVerifyTLS(cred.InsecureSkipTLSVerify),
getter.WithTLSClientConfig(cred.CertFile, cred.KeyFile, cred.CAFile),
getter.WithBasicAuth(cred.Username, cred.Password),
)
@@ -135,7 +128,7 @@ type RepoCredential struct {
// verify certificates of HTTPS-enabled servers using this CA bundle
CAFile string `json:"caFile,omitempty"`
// skip tls certificate checks for the repository, default is ture
InsecureSkipTLSVerify *bool `json:"insecureSkipTLSVerify,omitempty"`
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
S3Config `json:",inline"`
}