diff --git a/pkg/helm/actions/get_registry.go b/pkg/helm/actions/get_registry.go new file mode 100644 index 00000000000..f8266103861 --- /dev/null +++ b/pkg/helm/actions/get_registry.go @@ -0,0 +1,42 @@ +package actions + +import ( + "crypto/tls" + "fmt" + "net/http" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/registry" +) + +func GetDefaultOCIRegistry(conf *action.Configuration) error { + return GetOCIRegistry(conf, false, false) +} + +func GetOCIRegistry(conf *action.Configuration, insecure bool, plainHTTP bool) error { + if conf == nil { + return fmt.Errorf("action configuration cannot be nil") + } + opts := []registry.ClientOption{ + registry.ClientOptDebug(false), + } + if plainHTTP { + opts = append(opts, registry.ClientOptPlainHTTP()) + } + if insecure { + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + opts = append(opts, registry.ClientOptHTTPClient(httpClient)) + } + registryClient, err := registry.NewClient(opts...) + if err != nil { + return fmt.Errorf("failed to create registry client: %w", err) + } + conf.RegistryClient = registryClient + return nil +} diff --git a/pkg/helm/actions/get_registry_test.go b/pkg/helm/actions/get_registry_test.go new file mode 100644 index 00000000000..683d0d45b27 --- /dev/null +++ b/pkg/helm/actions/get_registry_test.go @@ -0,0 +1,52 @@ +package actions + +import ( + "io" + "testing" + + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chartutil" + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/storage" + "helm.sh/helm/v3/pkg/storage/driver" +) + +func TestGetDefaultOCIRegistry_Success(t *testing.T) { + store := storage.Init(driver.NewMemory()) + conf := &action.Configuration{ + RESTClientGetter: FakeConfig{}, + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, + Capabilities: chartutil.DefaultCapabilities, + } + require.Nil(t, conf.RegistryClient, "Registry Client should be nil") + + // Store original values + originalReleases := conf.Releases + originalKubeClient := conf.KubeClient + originalCapabilities := conf.Capabilities + + err := GetDefaultOCIRegistry(conf) + require.NoError(t, err) + require.NotNil(t, conf.RegistryClient, "Registry Client should not be nil") + + // Verify other configuration fields are not modified. + require.Equal(t, originalReleases, conf.Releases, "Releases should not be modified") + require.Equal(t, originalKubeClient, conf.KubeClient, "KubeClient should not be modified") + require.Equal(t, originalCapabilities, conf.Capabilities, "Capabilities should not be modified") + +} + +func TestGetDefaultOCIRegistry_NilConfig(t *testing.T) { + err := GetDefaultOCIRegistry(nil) + require.Error(t, err) + require.Contains(t, err.Error(), "action configuration cannot be nil") +} + +func TestGetDefaultOCIRegistry_MinimumConfig(t *testing.T) { + conf := &action.Configuration{} + err := GetDefaultOCIRegistry(conf) + require.NoError(t, err) + require.NotNil(t, conf.RegistryClient, "Registry Client should not be nil") +} diff --git a/pkg/helm/actions/install_chart.go b/pkg/helm/actions/install_chart.go index 9776b5ae3b6..d7b3b6d202f 100644 --- a/pkg/helm/actions/install_chart.go +++ b/pkg/helm/actions/install_chart.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "time" "github.com/openshift/api/helm/v1beta1" @@ -11,6 +12,7 @@ import ( "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/release" kv1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -195,3 +197,47 @@ func InstallChartAsync(ns, name, url string, vals map[string]interface{}, conf * } return &secret, nil } + +func InstallOCIChart(ns, name, url string, vals map[string]interface{}, conf *action.Configuration) (*release.Release, error) { + + // Accept OCI URLs (oci://...) or direct HTTP/HTTPS chart URLs (*.tgz) + isOCI := registry.IsOCI(url) + isDirectChartURL := strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") + isChartArchive := strings.HasSuffix(url, ".tgz") || strings.HasSuffix(url, ".tar.gz") + + if !isOCI && !(isDirectChartURL && isChartArchive) { + return nil, fmt.Errorf("invalid chart URL: %s, must be oci:// URL or http(s)://*.tgz", url) + } + + cmd := action.NewInstall(conf) + cmd.ReleaseName = name + cmd.Namespace = ns + + cp, err := cmd.ChartPathOptions.LocateChart(url, settings) + if err != nil { + return nil, fmt.Errorf("error locating chart: %v", err) + } + ch, err := loader.Load(cp) + if err != nil { + return nil, err + } + + // Add chart URL as an annotation before installation + if ch.Metadata == nil { + ch.Metadata = new(chart.Metadata) + } + if ch.Metadata.Annotations == nil { + ch.Metadata.Annotations = make(map[string]string) + } + ch.Metadata.Annotations["chart_url"] = url + + release, err := cmd.Run(ch, vals) + if err != nil { + return nil, err + } + if ch.Metadata.Name != "" && ch.Metadata.Version != "" { + metrics.HandleconsoleHelmInstallsTotal(ch.Metadata.Name, ch.Metadata.Version) + } + + return release, nil +} diff --git a/pkg/helm/actions/install_chart_test.go b/pkg/helm/actions/install_chart_test.go index cdf5035c51c..ee0313e7c64 100644 --- a/pkg/helm/actions/install_chart_test.go +++ b/pkg/helm/actions/install_chart_test.go @@ -402,3 +402,65 @@ func TestInstallChartAsync(t *testing.T) { }) } } + +func TestInstallOCIChart(t *testing.T) { + tests := []struct { + releaseName string + chartPath string + chartName string + chartVersion string + plainHTTP bool + insecureTLS bool + }{ + { + releaseName: "valid-chart-path", + chartPath: "http://localhost:9181/charts/influxdb-3.0.2.tgz", + chartName: "influxdb", + chartVersion: "3.0.2", + }, + { + releaseName: "valid-chart-path", + chartPath: "oci://localhost:5000/helm-charts/mychart:0.1.0", + chartName: "mychart", + chartVersion: "0.1.0", + plainHTTP: true, + }, + { + releaseName: "valid-chart-path", + chartPath: "oci://localhost:5443/helm-charts/mariadb:7.3.5", + chartName: "mariadb", + chartVersion: "7.3.5", + insecureTLS: true, + }, + { + releaseName: "invalid-chart-path", + chartPath: "http://localhost:9181/charts/influxdb/filename", + chartName: "influxdb", + chartVersion: "3.0.1", + }, + } + for _, tt := range tests { + t.Run(tt.releaseName, func(t *testing.T) { + store := storage.Init(driver.NewMemory()) + actionConfig := &action.Configuration{ + RESTClientGetter: FakeConfig{}, + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{Out: ioutil.Discard}, + Capabilities: chartutil.DefaultCapabilities, + Log: func(format string, v ...interface{}) {}, + } + GetOCIRegistry(actionConfig, tt.insecureTLS, tt.plainHTTP) + + rel, err := InstallOCIChart("test-namespace", tt.releaseName, tt.chartPath, nil, actionConfig) + if tt.releaseName == "valid-chart-path" { + require.NoError(t, err) + require.Equal(t, tt.releaseName, rel.Name) + require.Equal(t, tt.chartVersion, rel.Chart.Metadata.Version) + require.Equal(t, tt.chartPath, rel.Chart.Metadata.Annotations["chart_url"]) + + } else if tt.releaseName == "invalid-chart-path" { + require.Error(t, err) + } + }) + } +} diff --git a/pkg/helm/actions/setup_test.go b/pkg/helm/actions/setup_test.go index 47343a84482..db70bef41db 100644 --- a/pkg/helm/actions/setup_test.go +++ b/pkg/helm/actions/setup_test.go @@ -33,6 +33,9 @@ func TestMain(m *testing.M) { if err := ExecuteScript("./testdata/chartmuseum-stop.sh", false); err != nil { panic(err) } + if err := ExecuteScript("./testdata/zot-stop.sh", false); err != nil { + panic(err) + } if err := ExecuteScript("./testdata/cleanupNonTls.sh", false); err != nil { panic(err) } @@ -52,6 +55,15 @@ func setupTestWithTls() error { if err := ExecuteScript("./testdata/chartmuseum.sh", false); err != nil { return err } + if err := ExecuteScript("./testdata/downloadZot.sh", true); err != nil { + return err + } + if err := ExecuteScript("./testdata/downloadHelm.sh", false); err != nil { + return err + } + if err := ExecuteScript("./testdata/zot.sh", false); err != nil { + return err + } time.Sleep(5 * time.Second) if err := ExecuteScript("./testdata/cacertCreate.sh", true); err != nil { return err @@ -59,6 +71,9 @@ func setupTestWithTls() error { if err := ExecuteScript("./testdata/uploadCharts.sh", true); err != nil { return err } + if err := ExecuteScript("./testdata/uploadOciCharts.sh", true); err != nil { + return err + } return nil } @@ -66,10 +81,16 @@ func setupTestWithoutTls() error { if err := ExecuteScript("./testdata/chartmuseumWithoutTls.sh", false); err != nil { return err } + if err := ExecuteScript("./testdata/zotWithoutTls.sh", false); err != nil { + return err + } time.Sleep(5 * time.Second) if err := ExecuteScript("./testdata/uploadChartsWithoutTls.sh", true); err != nil { return err } + if err := ExecuteScript("./testdata/uploadOciChartsWithoutTls.sh", true); err != nil { + return err + } return nil } diff --git a/pkg/helm/actions/testdata/cleanup.sh b/pkg/helm/actions/testdata/cleanup.sh index 7cc03f9858e..35e8f09b8da 100755 --- a/pkg/helm/actions/testdata/cleanup.sh +++ b/pkg/helm/actions/testdata/cleanup.sh @@ -10,4 +10,7 @@ rm -rf ./server.key GOOS=${GOOS:-$(go env GOOS)} GOARCH=${GOARCH:-$(go env GOARCH)} rm -rf ./$GOOS-$GOARCH -rm -rf ./chartmuseum.tar.gz \ No newline at end of file +rm -rf ./chartmuseum.tar.gz +rm -rf ./helm.tar.gz +# Zot cleanup +rm -rf ./zot-storage-* \ No newline at end of file diff --git a/pkg/helm/actions/testdata/downloadHelm.sh b/pkg/helm/actions/testdata/downloadHelm.sh new file mode 100755 index 00000000000..1a7df94bebe --- /dev/null +++ b/pkg/helm/actions/testdata/downloadHelm.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Download Helm CLI for pushing OCI charts + +set -e +GOOS=${GOOS:-$(go env GOOS)} +GOARCH=${GOARCH:-$(go env GOARCH)} +HELM_VERSION=${HELM_VERSION:-3.19.0} +HELM_ARTIFACT_URL="https://get.helm.sh/helm-v${HELM_VERSION}-${GOOS}-${GOARCH}.tar.gz" + +mkdir -p "$GOOS-$GOARCH" + +if [ ! -f "$GOOS-$GOARCH/helm" ]; then + echo "Downloading Helm v${HELM_VERSION}..." + curl -L -o helm.tar.gz "$HELM_ARTIFACT_URL" + tar xzf helm.tar.gz --strip-components=1 -C "$GOOS-$GOARCH" "${GOOS}-${GOARCH}/helm" + chmod +x "$GOOS-$GOARCH/helm" +fi + +exit 0 + diff --git a/pkg/helm/actions/testdata/downloadZot.sh b/pkg/helm/actions/testdata/downloadZot.sh new file mode 100755 index 00000000000..1033e00a78d --- /dev/null +++ b/pkg/helm/actions/testdata/downloadZot.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e +GOOS=${GOOS:-$(go env GOOS)} +GOARCH=${GOARCH:-$(go env GOARCH)} +ZOT_VERSION=${ZOT_VERSION:-v2.1.6} +ZOT_ARTIFACT_URL="https://github.com/project-zot/zot/releases/download/$ZOT_VERSION/zot-$GOOS-$GOARCH" + +mkdir -p "$GOOS-$GOARCH" + +if [ ! -f "$GOOS-$GOARCH/zot" ]; then + curl -L -o "$GOOS-$GOARCH/zot" "$ZOT_ARTIFACT_URL" + chmod +x "$GOOS-$GOARCH/zot" +fi + +exit 0 + +# $GOOS-$GOARCH/zot is available from now on \ No newline at end of file diff --git a/pkg/helm/actions/testdata/uploadOciCharts.sh b/pkg/helm/actions/testdata/uploadOciCharts.sh new file mode 100755 index 00000000000..2900e0c0d8c --- /dev/null +++ b/pkg/helm/actions/testdata/uploadOciCharts.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Upload Helm charts as OCI artifacts to zot registry (with TLS) +set -e + +# Change to the script's parent directory (pkg/helm/actions/) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR/.." + +REGISTRY="localhost:5443" +CACERT="./cacert.pem" +CHARTS_DIR="../testdata" +GOOS=${GOOS:-$(go env GOOS)} +GOARCH=${GOARCH:-$(go env GOARCH)} + +# Use local helm binary if available, otherwise use system helm +if [ -x "./$GOOS-$GOARCH/helm" ]; then + HELM="./$GOOS-$GOARCH/helm" +elif command -v helm &> /dev/null; then + HELM="helm" +else + echo "Error: Helm not found. Run ./downloadHelm.sh first or install helm." + exit 1 +fi + +# Push charts to OCI registry using helm push +echo "Pushing mychart-0.1.0.tgz to oci://$REGISTRY/helm-charts..." +$HELM push $CHARTS_DIR/mychart-0.1.0.tgz oci://$REGISTRY/helm-charts --ca-file=$CACERT + +echo "Pushing mariadb-7.3.5.tgz to oci://$REGISTRY/helm-charts..." +$HELM push $CHARTS_DIR/mariadb-7.3.5.tgz oci://$REGISTRY/helm-charts --ca-file=$CACERT + +echo "Charts pushed successfully!" diff --git a/pkg/helm/actions/testdata/uploadOciChartsWithoutTls.sh b/pkg/helm/actions/testdata/uploadOciChartsWithoutTls.sh new file mode 100755 index 00000000000..dca4b114e8c --- /dev/null +++ b/pkg/helm/actions/testdata/uploadOciChartsWithoutTls.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Upload Helm charts as OCI artifacts to zot registry (without TLS) +set -e + +# Change to the script's parent directory (pkg/helm/actions/) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR/.." + +REGISTRY="localhost:5000" +CHARTS_DIR="../testdata" +GOOS=${GOOS:-$(go env GOOS)} +GOARCH=${GOARCH:-$(go env GOARCH)} + +# Use local helm binary if available, otherwise use system helm +if [ -x "./$GOOS-$GOARCH/helm" ]; then + HELM="./$GOOS-$GOARCH/helm" +elif command -v helm &> /dev/null; then + HELM="helm" +else + echo "Error: Helm not found. Run ./downloadHelm.sh first or install helm." + exit 1 +fi + +# Push charts to OCI registry using helm push +# Using --plain-http for non-TLS registry + +echo "Pushing mychart-0.1.0.tgz to oci://$REGISTRY/helm-charts..." +$HELM push $CHARTS_DIR/mychart-0.1.0.tgz oci://$REGISTRY/helm-charts --plain-http + +echo "Pushing mariadb-7.3.5.tgz to oci://$REGISTRY/helm-charts..." +$HELM push $CHARTS_DIR/mariadb-7.3.5.tgz oci://$REGISTRY/helm-charts --plain-http + +echo "Charts pushed successfully!" + diff --git a/pkg/helm/actions/testdata/zot-config-tls.json b/pkg/helm/actions/testdata/zot-config-tls.json new file mode 100644 index 00000000000..1a967c27ebf --- /dev/null +++ b/pkg/helm/actions/testdata/zot-config-tls.json @@ -0,0 +1,19 @@ +{ + "distSpecVersion": "1.1.0", + "storage": { + "rootDirectory": "./zot-storage-5443", + "gc": false + }, + "http": { + "address": "127.0.0.1", + "port": "5443", + "tls": { + "cert": "./server.crt", + "key": "./server.key" + } + }, + "log": { + "level": "debug" + } +} + diff --git a/pkg/helm/actions/testdata/zot-config.json b/pkg/helm/actions/testdata/zot-config.json new file mode 100644 index 00000000000..a63e073e3fe --- /dev/null +++ b/pkg/helm/actions/testdata/zot-config.json @@ -0,0 +1,15 @@ +{ + "distSpecVersion": "1.1.0", + "storage": { + "rootDirectory": "./zot-storage-5000", + "gc": false + }, + "http": { + "address": "127.0.0.1", + "port": "5000" + }, + "log": { + "level": "debug" + } +} + diff --git a/pkg/helm/actions/testdata/zot-stop.sh b/pkg/helm/actions/testdata/zot-stop.sh new file mode 100755 index 00000000000..456e3ee4215 --- /dev/null +++ b/pkg/helm/actions/testdata/zot-stop.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +/usr/bin/pkill -15 zot + diff --git a/pkg/helm/actions/testdata/zot.sh b/pkg/helm/actions/testdata/zot.sh new file mode 100755 index 00000000000..3354488fed7 --- /dev/null +++ b/pkg/helm/actions/testdata/zot.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Start zot OCI registry server with TLS +GOOS=${GOOS:-$(go env GOOS)} +GOARCH=${GOARCH:-$(go env GOARCH)} + +mkdir -p ./zot-storage-5443 + +./$GOOS-$GOARCH/zot serve ./testdata/zot-config-tls.json + diff --git a/pkg/helm/actions/testdata/zotWithoutTls.sh b/pkg/helm/actions/testdata/zotWithoutTls.sh new file mode 100755 index 00000000000..1a5e0aef2f6 --- /dev/null +++ b/pkg/helm/actions/testdata/zotWithoutTls.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Start zot OCI registry server without TLS +GOOS=${GOOS:-$(go env GOOS)} +GOARCH=${GOARCH:-$(go env GOARCH)} + +mkdir -p ./zot-storage-5000 + +./$GOOS-$GOARCH/zot serve ./testdata/zot-config.json + diff --git a/pkg/helm/handlers/handler_test.go b/pkg/helm/handlers/handler_test.go index e80bf4d7994..f14cedae3b3 100644 --- a/pkg/helm/handlers/handler_test.go +++ b/pkg/helm/handlers/handler_test.go @@ -63,6 +63,7 @@ var fakeReleaseManifest = "manifest-data" func fakeHelmHandler() helmHandlers { return helmHandlers{ getActionConfigurations: getFakeActionConfigurations, + getDefaultOCIRegistry: fakeGetDefaultOCIRegistry, } } @@ -201,6 +202,16 @@ func getFakeActionConfigurations(string, string, string, *http.RoundTripper) *ac } } +func fakeGetDefaultOCIRegistry(conf *action.Configuration) error { + return nil +} + +func fakeInstallOCIChart(mockedRelease *release.Release, err error) func(ns string, name string, url string, values map[string]interface{}, conf *action.Configuration) (*release.Release, error) { + return func(ns string, name string, url string, values map[string]interface{}, conf *action.Configuration) (*release.Release, error) { + return mockedRelease, err + } +} + func TestHelmHandlers_HandleHelmList(t *testing.T) { tests := []struct { name string @@ -1051,3 +1062,63 @@ func TestHelmHandlers_HandleHelmUnInstallAsync(t *testing.T) { }) } } + +func TestHelmHandlers_HandleInstallOCIChart(t *testing.T) { + tests := []struct { + name string + requestBody string + expectedResponse string + installedRelease release.Release + error + httpStatusCode int + }{ + { + name: "Invalid JSON request", + requestBody: `{invalid}`, + expectedResponse: `{"error":"Failed to parse request: invalid character 'i' looking for beginning of object key string"}`, + httpStatusCode: http.StatusBadGateway, + }, + { + name: "Error occurred during OCI chart installation", + requestBody: `{"name":"test-release","namespace":"default","chartUrl":"http://ghcr.io/test/chart"}`, + expectedResponse: `{"error":"Failed to install helm chart: Chart path is invalid"}`, + error: errors.New("Chart path is invalid"), + httpStatusCode: http.StatusBadGateway, + }, + { + name: "Successful OCI chart install returns release info", + requestBody: `{"name":"test-release","namespace":"default","chartUrl":"oci://ghcr.io/test/chart"}`, + installedRelease: fakeRelease, + httpStatusCode: http.StatusCreated, + expectedResponse: `{"name":"Test"}`, + }, + { + name: "Successful HTTP .tgz chart install returns release info", + requestBody: `{"name":"test-release","namespace":"default","chartUrl":"https://charts.example.com/mychart-1.0.0.tgz"}`, + installedRelease: fakeRelease, + httpStatusCode: http.StatusCreated, + expectedResponse: `{"name":"Test"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handlers := fakeHelmHandler() + handlers.installOCIChart = fakeInstallOCIChart(&tt.installedRelease, tt.error) + + request := httptest.NewRequest("POST", "/api/helm/release/oci", strings.NewReader(tt.requestBody)) + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + + handlers.HandleInstallOCIChart(&auth.User{}, response, request) + if response.Code != tt.httpStatusCode { + t.Errorf("response code should be %v but got %v", tt.httpStatusCode, response.Code) + } + if response.Header().Get("Content-Type") != "application/json" { + t.Errorf("content type should be application/json but got %s", response.Header().Get("Content-Type")) + } + if response.Body.String() != tt.expectedResponse { + t.Errorf("response body not matching expected is %s and received is %s", tt.expectedResponse, response.Body.String()) + } + }) + } +} diff --git a/pkg/helm/handlers/handlers.go b/pkg/helm/handlers/handlers.go index 879e87bc870..64e25ae708d 100644 --- a/pkg/helm/handlers/handlers.go +++ b/pkg/helm/handlers/handlers.go @@ -30,6 +30,7 @@ func New(apiUrl string, transport http.RoundTripper, kubeversionGetter version.K renderManifests: actions.RenderManifests, installChartAsync: actions.InstallChartAsync, installChart: actions.InstallChart, + installOCIChart: actions.InstallOCIChart, listReleases: actions.ListReleases, getRelease: actions.GetRelease, getChart: actions.GetChart, @@ -39,6 +40,7 @@ func New(apiUrl string, transport http.RoundTripper, kubeversionGetter version.K uninstallReleaseAsync: actions.UninstallReleaseAsync, rollbackRelease: actions.RollbackRelease, getReleaseHistory: actions.GetReleaseHistory, + getDefaultOCIRegistry: actions.GetDefaultOCIRegistry, } h.newProxy = func(bearerToken string) (getter chartproxy.Proxy, err error) { @@ -62,6 +64,7 @@ type helmHandlers struct { renderManifests func(string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, string, string, bool) (string, error) installChartAsync func(string, string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, bool, string) (*kv1.Secret, error) installChart func(string, string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, bool, string) (*release.Release, error) + installOCIChart func(string, string, string, map[string]interface{}, *action.Configuration) (*release.Release, error) listReleases func(*action.Configuration, bool) ([]*release.Release, error) upgradeReleaseAsync func(string, string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, bool, string) (*kv1.Secret, error) upgradeRelease func(string, string, string, map[string]interface{}, *action.Configuration, dynamic.Interface, corev1client.CoreV1Interface, bool, string) (*release.Release, error) @@ -72,6 +75,7 @@ type helmHandlers struct { getChart func(chartUrl string, conf *action.Configuration, namespace string, client dynamic.Interface, coreClient corev1client.CoreV1Interface, filesCleanup bool, indexEntry string) (*chart.Chart, error) getReleaseHistory func(releaseName string, conf *action.Configuration) ([]*release.Release, error) newProxy func(bearerToken string) (chartproxy.Proxy, error) + getDefaultOCIRegistry func(*action.Configuration) error } func (h *helmHandlers) restConfig(bearerToken string) *rest.Config { @@ -127,6 +131,11 @@ func (h *helmHandlers) HandleHelmInstall(user *auth.User, w http.ResponseWriter, } conf := h.getActionConfigurations(h.ApiServerHost, req.Namespace, user.Token, &h.Transport) + err = h.getDefaultOCIRegistry(conf) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to get default registry: %v", err)}) + return + } restConfig, err := conf.RESTClientGetter.ToRESTConfig() if err != nil { serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to parse request: %v", err)}) @@ -163,6 +172,11 @@ func (h *helmHandlers) HandleHelmInstallAsync(user *auth.User, w http.ResponseWr } conf := h.getActionConfigurations(h.ApiServerHost, req.Namespace, user.Token, &h.Transport) + err = h.getDefaultOCIRegistry(conf) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to get default registry: %v", err)}) + return + } restConfig, err := conf.RESTClientGetter.ToRESTConfig() if err != nil { serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to parse request: %v", err)}) @@ -240,6 +254,11 @@ func (h *helmHandlers) HandleChartGet(user *auth.User, w http.ResponseWriter, r indexEntry := params.Get("indexEntry") // scope request to default namespace conf := h.getActionConfigurations(h.ApiServerHost, "default", user.Token, &h.Transport) + err := h.getDefaultOCIRegistry(conf) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to get default registry: %v", err)}) + return + } restConfig, err := conf.RESTClientGetter.ToRESTConfig() if err != nil { serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to parse request: %v", err)}) @@ -277,6 +296,11 @@ func (h *helmHandlers) HandleUpgradeRelease(user *auth.User, w http.ResponseWrit } conf := h.getActionConfigurations(h.ApiServerHost, req.Namespace, user.Token, &h.Transport) + err = h.getDefaultOCIRegistry(conf) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to get default registry: %v", err)}) + return + } restConfig, err := conf.RESTClientGetter.ToRESTConfig() if err != nil { serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to parse request: %v", err)}) @@ -313,6 +337,11 @@ func (h *helmHandlers) HandleUpgradeReleaseAsync(user *auth.User, w http.Respons } conf := h.getActionConfigurations(h.ApiServerHost, req.Namespace, user.Token, &h.Transport) + err = h.getDefaultOCIRegistry(conf) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to get default registry: %v", err)}) + return + } restConfig, err := conf.RESTClientGetter.ToRESTConfig() if err != nil { serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to parse request: %v", err)}) @@ -366,6 +395,11 @@ func (h *helmHandlers) HandleRollbackRelease(user *auth.User, w http.ResponseWri } conf := h.getActionConfigurations(h.ApiServerHost, req.Namespace, user.Token, &h.Transport) + err = h.getDefaultOCIRegistry(conf) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to get default registry: %v", err)}) + return + } rel, err := h.rollbackRelease(req.Name, req.Version, conf) if err != nil { if err.Error() == actions.ErrReleaseRevisionNotFound.Error() { @@ -464,3 +498,27 @@ func (h *helmHandlers) HandleUninstallReleaseAsync(user *auth.User, w http.Respo } w.WriteHeader(http.StatusNoContent) } + +func (h *helmHandlers) HandleInstallOCIChart(user *auth.User, w http.ResponseWriter, r *http.Request) { + var req HelmRequest + + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to parse request: %v", err)}) + return + } + + conf := h.getActionConfigurations(h.ApiServerHost, "default", user.Token, &h.Transport) + err = h.getDefaultOCIRegistry(conf) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, + serverutils.ApiError{Err: fmt.Sprintf("Failed to get default registry: %v", err)}) + return + } + resp, err := h.installOCIChart(req.Namespace, req.Name, req.ChartUrl, req.Values, conf) + if err != nil { + serverutils.SendResponse(w, http.StatusBadGateway, serverutils.ApiError{Err: fmt.Sprintf("Failed to install helm chart: %v", err)}) + return + } + serverutils.SendResponse(w, http.StatusCreated, resp) +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 728f0b4d6d0..7c88f780cbe 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -635,6 +635,7 @@ func (s *Server) HTTPHandler() (http.Handler, error) { handle("/api/helm/template", authHandlerWithUser(helmHandlers.HandleHelmRenderManifests)) handle("/api/helm/releases", authHandlerWithUser(helmHandlers.HandleHelmList)) handle("/api/helm/chart", authHandlerWithUser(helmHandlers.HandleChartGet)) + handle("/api/helm/oci-chart", authHandlerWithUser(helmHandlers.HandleInstallOCIChart)) handle("/api/helm/release/history", authHandlerWithUser(helmHandlers.HandleGetReleaseHistory)) handle("/api/helm/charts/index.yaml", authHandlerWithUser(helmHandlers.HandleIndexFile))