From 7a8a20e0cc3b3a9697bca1511dc1c1fea6135252 Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Fri, 14 Apr 2023 14:50:37 +1200 Subject: [PATCH] feat(libhelm): allow passing optional env and http client [EE-5252] (#8758) --- api/http/handler/helm/user_helm_repos.go | 2 +- api/http/handler/settings/settings_update.go | 2 +- .../helm-add-repository.html | 5 +- app/portainer/services/api/sslService.js | 4 +- app/portainer/settings/general/index.js | 3 +- .../general/ssl-ca-file-settings/index.js | 6 ++ .../ssl-ca-file-settings-controller.js | 9 +++ .../ssl-ca-file-settings.html | 43 ++++++++++++++ .../ssl-certificate.controller.js | 7 +-- .../ssl-certificate/ssl-certificate.html | 59 +++++++++++++------ app/portainer/views/settings/settings.html | 1 + app/react/portainer/feature-flags/enums.ts | 1 + .../feature-flags/feature-flags.service.ts | 1 + pkg/libhelm/binary/get.go | 2 +- pkg/libhelm/binary/helm_package.go | 10 +++- pkg/libhelm/binary/install.go | 2 +- pkg/libhelm/binary/install_test.go | 8 +-- pkg/libhelm/binary/list.go | 2 +- pkg/libhelm/binary/search_repo.go | 15 +++-- pkg/libhelm/binary/show.go | 2 +- pkg/libhelm/binary/uninstall.go | 2 +- pkg/libhelm/options/get_options.go | 2 + pkg/libhelm/options/install_options.go | 3 + pkg/libhelm/options/list_options.go | 2 + pkg/libhelm/options/search_repo_options.go | 5 +- pkg/libhelm/options/show_options.go | 2 + pkg/libhelm/options/uninstall_options.go | 2 + pkg/libhelm/validate_repo.go | 9 ++- pkg/libhelm/validate_repo_test.go | 2 +- 29 files changed, 161 insertions(+), 52 deletions(-) create mode 100644 app/portainer/settings/general/ssl-ca-file-settings/index.js create mode 100644 app/portainer/settings/general/ssl-ca-file-settings/ssl-ca-file-settings-controller.js create mode 100644 app/portainer/settings/general/ssl-ca-file-settings/ssl-ca-file-settings.html diff --git a/api/http/handler/helm/user_helm_repos.go b/api/http/handler/helm/user_helm_repos.go index d183574fc..b45eba2fd 100644 --- a/api/http/handler/helm/user_helm_repos.go +++ b/api/http/handler/helm/user_helm_repos.go @@ -25,7 +25,7 @@ type addHelmRepoUrlPayload struct { } func (p *addHelmRepoUrlPayload) Validate(_ *http.Request) error { - return libhelm.ValidateHelmRepositoryURL(p.URL) + return libhelm.ValidateHelmRepositoryURL(p.URL, nil) } // @id HelmUserRepositoryCreate diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index e328f9de0..20d143bff 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -143,7 +143,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/") if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL { - err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL) + err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL, nil) if err != nil { return httperror.BadRequest("Invalid Helm repository URL. Must correspond to a valid URL format", err) } diff --git a/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.html b/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.html index 17d23988d..275ba4847 100644 --- a/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.html +++ b/app/kubernetes/components/helm/helm-templates/helm-add-repository/helm-add-repository.html @@ -11,7 +11,10 @@
- Add a Helm repository. All Helm charts in the repository will be added to the list. + + + Add a Helm repository. All Helm charts in the repository will be added to the list. +
diff --git a/app/portainer/services/api/sslService.js b/app/portainer/services/api/sslService.js index ed3a20ae9..387b289c1 100644 --- a/app/portainer/services/api/sslService.js +++ b/app/portainer/services/api/sslService.js @@ -13,7 +13,7 @@ function SSLServiceFactory(SSL) { return SSL.get().$promise; } - function upload(httpEnabled, cert, key) { - return SSL.upload({ httpEnabled, cert, key }).$promise; + function upload(SSLPayload) { + return SSL.upload(SSLPayload).$promise; } } diff --git a/app/portainer/settings/general/index.js b/app/portainer/settings/general/index.js index ab9158702..fa01c87b7 100644 --- a/app/portainer/settings/general/index.js +++ b/app/portainer/settings/general/index.js @@ -1,5 +1,6 @@ import angular from 'angular'; import { sslCertificate } from './ssl-certificate'; +import { sslCaFileSettings } from './ssl-ca-file-settings'; -export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).name; +export default angular.module('portainer.settings.general', []).component('sslCertificateSettings', sslCertificate).component('sslCaFileSettings', sslCaFileSettings).name; diff --git a/app/portainer/settings/general/ssl-ca-file-settings/index.js b/app/portainer/settings/general/ssl-ca-file-settings/index.js new file mode 100644 index 000000000..0dab9a576 --- /dev/null +++ b/app/portainer/settings/general/ssl-ca-file-settings/index.js @@ -0,0 +1,6 @@ +import controller from './ssl-ca-file-settings-controller.js'; + +export const sslCaFileSettings = { + templateUrl: './ssl-ca-file-settings.html', + controller, +}; diff --git a/app/portainer/settings/general/ssl-ca-file-settings/ssl-ca-file-settings-controller.js b/app/portainer/settings/general/ssl-ca-file-settings/ssl-ca-file-settings-controller.js new file mode 100644 index 000000000..acf14c53f --- /dev/null +++ b/app/portainer/settings/general/ssl-ca-file-settings/ssl-ca-file-settings-controller.js @@ -0,0 +1,9 @@ +import { FeatureId } from '@/react/portainer/feature-flags/enums'; +class SslCaFileSettingsController { + /* @ngInject */ + constructor() { + this.limitedFeature = FeatureId.CA_FILE; + } +} + +export default SslCaFileSettingsController; diff --git a/app/portainer/settings/general/ssl-ca-file-settings/ssl-ca-file-settings.html b/app/portainer/settings/general/ssl-ca-file-settings/ssl-ca-file-settings.html new file mode 100644 index 000000000..c63e63fd3 --- /dev/null +++ b/app/portainer/settings/general/ssl-ca-file-settings/ssl-ca-file-settings.html @@ -0,0 +1,43 @@ +
+
+ +
+ + + + + + + Provide an additional CA file containing certificate(s) for HTTPS connections to Helm repositories. + + + +
+
+ + CA File + + + + + + +
+
+ +
+
+ +
+
+ +
+
+
+
+
diff --git a/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js b/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js index d79b7500b..c5dfd4dda 100644 --- a/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js +++ b/app/portainer/settings/general/ssl-certificate/ssl-certificate.controller.js @@ -21,9 +21,8 @@ class SslCertificateController { reloadingPage: false, }; - const pemPattern = '.pem'; - this.certFilePattern = `${pemPattern},.crt,.cer,.cert`; - this.keyFilePattern = `${pemPattern},.key`; + this.certFilePattern = `.pem,.crt,.cer,.cert`; + this.keyFilePattern = `.pem,.key`; this.save = this.save.bind(this); this.onChangeForceHTTPS = this.onChangeForceHTTPS.bind(this); @@ -46,7 +45,7 @@ class SslCertificateController { const cert = this.formValues.certFile ? await this.formValues.certFile.text() : null; const key = this.formValues.keyFile ? await this.formValues.keyFile.text() : null; const httpEnabled = !this.formValues.forceHTTPS; - await this.SSLService.upload(httpEnabled, cert, key); + await this.SSLService.upload({ httpEnabled, cert, key }); await new Promise((resolve) => setTimeout(resolve, 2000)); location.reload(); diff --git a/app/portainer/settings/general/ssl-certificate/ssl-certificate.html b/app/portainer/settings/general/ssl-certificate/ssl-certificate.html index d23c4f9ef..921068e14 100644 --- a/app/portainer/settings/general/ssl-certificate/ssl-certificate.html +++ b/app/portainer/settings/general/ssl-certificate/ssl-certificate.html @@ -16,22 +16,33 @@ label="'Force HTTPS only'" on-change="($ctrl.onChangeForceHTTPS)" field-class="'col-sm-12'" - label-class="'col-sm-2'" + label-class="'col-sm-3 col-lg-2'" >
-
- Provide a new SSL Certificate to replace the existing one that is used for HTTPS connections. -
+ + + Provide a new SSL Certificate to replace the existing one that is used for HTTPS connections. + +
- Upload a X.509 certificate, commonly a crt, a cer, or a pem file. -
- -
-
- - +
+ + SSL/TLS certificate + + + + {{ $ctrl.formValues.certFile.name }} @@ -48,14 +59,24 @@
+
- Upload a private key, commonly a key, or a pem file. -
- -
-
- - +
+ + SSL/TLS private key + + + + {{ $ctrl.formValues.keyFile.name }} @@ -76,7 +97,7 @@
+
diff --git a/app/react/portainer/feature-flags/enums.ts b/app/react/portainer/feature-flags/enums.ts index 4c9efa8d6..49a238bbd 100644 --- a/app/react/portainer/feature-flags/enums.ts +++ b/app/react/portainer/feature-flags/enums.ts @@ -40,4 +40,5 @@ export enum FeatureId { K8S_ROLLING_RESTART = 'k8s-rolling-restart', K8SINSTALL = 'k8s-install', K8S_ANNOTATIONS = 'k8s-annotations', + CA_FILE = 'ca-file', } diff --git a/app/react/portainer/feature-flags/feature-flags.service.ts b/app/react/portainer/feature-flags/feature-flags.service.ts index a16559ab7..ba8ceddcb 100644 --- a/app/react/portainer/feature-flags/feature-flags.service.ts +++ b/app/react/portainer/feature-flags/feature-flags.service.ts @@ -45,6 +45,7 @@ export async function init(edition: Edition) { [FeatureId.K8S_ADM_ONLY_USR_INGRESS_DEPLY]: Edition.BE, [FeatureId.K8S_ROLLING_RESTART]: Edition.BE, [FeatureId.K8S_ANNOTATIONS]: Edition.BE, + [FeatureId.CA_FILE]: Edition.BE, }; state.currentEdition = currentEdition; diff --git a/pkg/libhelm/binary/get.go b/pkg/libhelm/binary/get.go index 883a61a2d..0df41bbc8 100644 --- a/pkg/libhelm/binary/get.go +++ b/pkg/libhelm/binary/get.go @@ -20,7 +20,7 @@ func (hbpm *helmBinaryPackageManager) Get(getOpts options.GetOptions) ([]byte, e args = append(args, "--namespace", getOpts.Namespace) } - result, err := hbpm.runWithKubeConfig("get", args, getOpts.KubernetesClusterAccess) + result, err := hbpm.runWithKubeConfig("get", args, getOpts.KubernetesClusterAccess, getOpts.Env) if err != nil { return nil, errors.Wrap(err, "failed to run helm get on specified args") } diff --git a/pkg/libhelm/binary/helm_package.go b/pkg/libhelm/binary/helm_package.go index 73805a89e..2e64a8a2b 100644 --- a/pkg/libhelm/binary/helm_package.go +++ b/pkg/libhelm/binary/helm_package.go @@ -2,6 +2,7 @@ package binary import ( "bytes" + "os" "os/exec" "path" "runtime" @@ -21,7 +22,7 @@ func NewHelmBinaryPackageManager(binaryPath string) *helmBinaryPackageManager { } // runWithKubeConfig will execute run against the provided Kubernetes cluster with kubeconfig as cli arguments. -func (hbpm *helmBinaryPackageManager) runWithKubeConfig(command string, args []string, kca *options.KubernetesClusterAccess) ([]byte, error) { +func (hbpm *helmBinaryPackageManager) runWithKubeConfig(command string, args []string, kca *options.KubernetesClusterAccess, env []string) ([]byte, error) { cmdArgs := make([]string, 0) if kca != nil { cmdArgs = append(cmdArgs, "--kube-apiserver", kca.ClusterServerURL) @@ -29,13 +30,13 @@ func (hbpm *helmBinaryPackageManager) runWithKubeConfig(command string, args []s cmdArgs = append(cmdArgs, "--kube-ca-file", kca.CertificateAuthorityFile) } cmdArgs = append(cmdArgs, args...) - return hbpm.run(command, cmdArgs) + return hbpm.run(command, cmdArgs, env) } // run will execute helm command against the provided Kubernetes cluster. // The endpointId and authToken are dynamic params (based on the user) that allow helm to execute commands // in the context of the current user against specified k8s cluster. -func (hbpm *helmBinaryPackageManager) run(command string, args []string) ([]byte, error) { +func (hbpm *helmBinaryPackageManager) run(command string, args []string, env []string) ([]byte, error) { cmdArgs := make([]string, 0) cmdArgs = append(cmdArgs, command) cmdArgs = append(cmdArgs, args...) @@ -49,6 +50,9 @@ func (hbpm *helmBinaryPackageManager) run(command string, args []string) ([]byte cmd := exec.Command(helmPath, cmdArgs...) cmd.Stderr = &stderr + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, env...) + output, err := cmd.Output() if err != nil { return nil, errors.Wrap(err, stderr.String()) diff --git a/pkg/libhelm/binary/install.go b/pkg/libhelm/binary/install.go index 24798517d..dbd2e5dd0 100644 --- a/pkg/libhelm/binary/install.go +++ b/pkg/libhelm/binary/install.go @@ -33,7 +33,7 @@ func (hbpm *helmBinaryPackageManager) Install(installOpts options.InstallOptions args = append(args, "--post-renderer", installOpts.PostRenderer) } - result, err := hbpm.runWithKubeConfig("install", args, installOpts.KubernetesClusterAccess) + result, err := hbpm.runWithKubeConfig("install", args, installOpts.KubernetesClusterAccess, installOpts.Env) if err != nil { return nil, errors.Wrap(err, "failed to run helm install on specified args") } diff --git a/pkg/libhelm/binary/install_test.go b/pkg/libhelm/binary/install_test.go index f22d5916a..e3252e1f2 100644 --- a/pkg/libhelm/binary/install_test.go +++ b/pkg/libhelm/binary/install_test.go @@ -61,7 +61,7 @@ func Test_Install(t *testing.T) { } release, err := hbpm.Install(installOpts) - defer hbpm.run("uninstall", []string{"test-nginx"}) + defer hbpm.run("uninstall", []string{"test-nginx"}, nil) is.NoError(err, "should successfully install release", release) }) @@ -73,7 +73,7 @@ func Test_Install(t *testing.T) { Repo: "https://charts.bitnami.com/bitnami", } release, err := hbpm.Install(installOpts) - defer hbpm.run("uninstall", []string{release.Name}) + defer hbpm.run("uninstall", []string{release.Name}, nil) is.NoError(err, "should successfully install release", release) }) @@ -92,7 +92,7 @@ func Test_Install(t *testing.T) { ValuesFile: values, } release, err := hbpm.Install(installOpts) - defer hbpm.run("uninstall", []string{"test-nginx-2"}) + defer hbpm.run("uninstall", []string{"test-nginx-2"}, nil) is.NoError(err, "should successfully install release", release) }) @@ -105,7 +105,7 @@ func Test_Install(t *testing.T) { Repo: "https://portainer.github.io/k8s/", } release, err := hbpm.Install(installOpts) - defer hbpm.run("uninstall", []string{installOpts.Name}) + defer hbpm.run("uninstall", []string{installOpts.Name}, nil) is.NoError(err, "should successfully install release", release) }) diff --git a/pkg/libhelm/binary/list.go b/pkg/libhelm/binary/list.go index a81f511bf..7859c77f4 100644 --- a/pkg/libhelm/binary/list.go +++ b/pkg/libhelm/binary/list.go @@ -23,7 +23,7 @@ func (hbpm *helmBinaryPackageManager) List(listOpts options.ListOptions) ([]rele args = append(args, "--namespace", listOpts.Namespace) } - result, err := hbpm.runWithKubeConfig("list", args, listOpts.KubernetesClusterAccess) + result, err := hbpm.runWithKubeConfig("list", args, listOpts.KubernetesClusterAccess, listOpts.Env) if err != nil { return []release.ReleaseElement{}, errors.Wrap(err, "failed to run helm list on specified args") } diff --git a/pkg/libhelm/binary/search_repo.go b/pkg/libhelm/binary/search_repo.go index a959f512e..465c0d7f4 100644 --- a/pkg/libhelm/binary/search_repo.go +++ b/pkg/libhelm/binary/search_repo.go @@ -50,12 +50,15 @@ func (hbpm *helmBinaryPackageManager) SearchRepo(searchRepoOpts options.SearchRe return nil, errRequiredSearchOptions } - // The current index.yaml is ~9MB on bitnami. - // At a slow @2mbit download = 40s. @100bit = ~1s. - // I'm seeing 3 - 4s over wifi. - // Give ample time but timeout for now. Can be improved in the future - client := http.Client{ - Timeout: 60 * time.Second, + client := searchRepoOpts.Client + if searchRepoOpts.Client == nil { + // The current index.yaml is ~9MB on bitnami. + // At a slow @2mbit download = 40s. @100bit = ~1s. + // I'm seeing 3 - 4s over wifi. + // Give ample time but timeout for now. Can be improved in the future + client = &http.Client{ + Timeout: 60 * time.Second, + } } url, err := url.ParseRequestURI(searchRepoOpts.Repo) diff --git a/pkg/libhelm/binary/show.go b/pkg/libhelm/binary/show.go index 7639e6dc2..4fe49aeef 100644 --- a/pkg/libhelm/binary/show.go +++ b/pkg/libhelm/binary/show.go @@ -20,7 +20,7 @@ func (hbpm *helmBinaryPackageManager) Show(showOpts options.ShowOptions) ([]byte "--repo", showOpts.Repo, } - result, err := hbpm.run("show", args) + result, err := hbpm.run("show", args, showOpts.Env) if err != nil { return nil, errors.Wrap(err, "failed to run helm show on specified args") } diff --git a/pkg/libhelm/binary/uninstall.go b/pkg/libhelm/binary/uninstall.go index d8a7fdfc6..b926e8ee5 100644 --- a/pkg/libhelm/binary/uninstall.go +++ b/pkg/libhelm/binary/uninstall.go @@ -20,7 +20,7 @@ func (hbpm *helmBinaryPackageManager) Uninstall(uninstallOpts options.UninstallO args = append(args, "--namespace", uninstallOpts.Namespace) } - _, err := hbpm.runWithKubeConfig("uninstall", args, uninstallOpts.KubernetesClusterAccess) + _, err := hbpm.runWithKubeConfig("uninstall", args, uninstallOpts.KubernetesClusterAccess, uninstallOpts.Env) if err != nil { return errors.Wrap(err, "failed to run helm uninstall on specified args") } diff --git a/pkg/libhelm/options/get_options.go b/pkg/libhelm/options/get_options.go index fb857eb95..a65cd75a7 100644 --- a/pkg/libhelm/options/get_options.go +++ b/pkg/libhelm/options/get_options.go @@ -17,4 +17,6 @@ type GetOptions struct { Namespace string ReleaseResource releaseResource KubernetesClusterAccess *KubernetesClusterAccess + + Env []string } diff --git a/pkg/libhelm/options/install_options.go b/pkg/libhelm/options/install_options.go index 0ccef33a3..5cc6081fb 100644 --- a/pkg/libhelm/options/install_options.go +++ b/pkg/libhelm/options/install_options.go @@ -9,4 +9,7 @@ type InstallOptions struct { ValuesFile string PostRenderer string KubernetesClusterAccess *KubernetesClusterAccess + + // Optional environment vars to pass when running helm + Env []string } diff --git a/pkg/libhelm/options/list_options.go b/pkg/libhelm/options/list_options.go index 57a520dab..72e0b2562 100644 --- a/pkg/libhelm/options/list_options.go +++ b/pkg/libhelm/options/list_options.go @@ -6,4 +6,6 @@ type ListOptions struct { Selector string Namespace string KubernetesClusterAccess *KubernetesClusterAccess + + Env []string } diff --git a/pkg/libhelm/options/search_repo_options.go b/pkg/libhelm/options/search_repo_options.go index da937c21c..73acc5709 100644 --- a/pkg/libhelm/options/search_repo_options.go +++ b/pkg/libhelm/options/search_repo_options.go @@ -1,5 +1,8 @@ package options +import "net/http" + type SearchRepoOptions struct { - Repo string + Repo string `example:"https://charts.gitlab.io/"` + Client *http.Client `example:"&http.Client{Timeout: time.Second * 10}"` } diff --git a/pkg/libhelm/options/show_options.go b/pkg/libhelm/options/show_options.go index 2da79b945..c94e14d80 100644 --- a/pkg/libhelm/options/show_options.go +++ b/pkg/libhelm/options/show_options.go @@ -19,4 +19,6 @@ type ShowOptions struct { OutputFormat ShowOutputFormat Chart string Repo string + + Env []string } diff --git a/pkg/libhelm/options/uninstall_options.go b/pkg/libhelm/options/uninstall_options.go index 01753b306..6363e763a 100644 --- a/pkg/libhelm/options/uninstall_options.go +++ b/pkg/libhelm/options/uninstall_options.go @@ -5,4 +5,6 @@ type UninstallOptions struct { Name string Namespace string KubernetesClusterAccess *KubernetesClusterAccess + + Env []string } diff --git a/pkg/libhelm/validate_repo.go b/pkg/libhelm/validate_repo.go index 19cf6eabb..b11dfa4d3 100644 --- a/pkg/libhelm/validate_repo.go +++ b/pkg/libhelm/validate_repo.go @@ -13,7 +13,7 @@ import ( const invalidChartRepo = "%q is not a valid chart repository or cannot be reached" -func ValidateHelmRepositoryURL(repoUrl string) error { +func ValidateHelmRepositoryURL(repoUrl string, client *http.Client) error { if repoUrl == "" { return errors.New("URL is required") } @@ -29,9 +29,12 @@ func ValidateHelmRepositoryURL(repoUrl string) error { url.Path = path.Join(url.Path, "index.yaml") - var client = &http.Client{ - Timeout: time.Second * 10, + if client == nil { + client = &http.Client{ + Timeout: time.Second * 10, + } } + response, err := client.Head(url.String()) if err != nil { return errors.Wrapf(err, invalidChartRepo, repoUrl) diff --git a/pkg/libhelm/validate_repo_test.go b/pkg/libhelm/validate_repo_test.go index 1df96531a..ea04a3818 100644 --- a/pkg/libhelm/validate_repo_test.go +++ b/pkg/libhelm/validate_repo_test.go @@ -37,7 +37,7 @@ func Test_ValidateHelmRepositoryURL(t *testing.T) { func(tc testCase) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - err := ValidateHelmRepositoryURL(tc.url) + err := ValidateHelmRepositoryURL(tc.url, nil) if tc.invalid { is.Errorf(err, "error expected: %s", tc.url) } else {