From a13cab65b2f3a705b03db95ebff0fa5c2d8ebf40 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 16:10:48 +0800 Subject: [PATCH] [Backport release-1.3] Feat: support basic auth private helm repo (#3631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * support auth Signed-off-by: 楚岳 (cherry picked from commit 54c05afb1a9b860cfc0d6091ecf252078ac386a2) * add test Signed-off-by: 楚岳 fix check diff Signed-off-by: 楚岳 fix test fix add comments fix test (cherry picked from commit a8961ec8cc29520fc89a4717d848ce97fdffcb31) * add tests Signed-off-by: 楚岳 fix add more test Signed-off-by: 楚岳 (cherry picked from commit 4f45a6af8e21cc4a66eea42814f4e81d3ee4c27f) * add more test Signed-off-by: 楚岳 (cherry picked from commit dee791aa51ae6b4f1600b0248ee39aaaaa7498a4) * extract set auth info as a global func Signed-off-by: 楚岳 (cherry picked from commit f8fb0137e31a58e9073274966357dbe298c12493) * return bcode Signed-off-by: 楚岳 (cherry picked from commit 057a67d8b9a39fdeefb5ea15998ceaa6f85e940f) Co-authored-by: 楚岳 --- pkg/addon/versioned_registry.go | 6 +- pkg/apiserver/rest/usecase/helm.go | 33 +++++- pkg/apiserver/rest/usecase/helm_test.go | 102 ++++++++++++++++++ .../rest/usecase/testdata/helm/index.yaml | 36 +++++++ .../usecase/testdata/helm/mysql-8.8.23.tgz | Bin 0 -> 39646 bytes .../rest/utils/bcode/013_repository.go | 3 + pkg/utils/common/common.go | 13 ++- pkg/utils/common/common_test.go | 87 ++++++++++++++- pkg/utils/helm/helm.go | 15 +++ pkg/utils/helm/helm_helper.go | 22 ++-- pkg/utils/helm/helm_helper_test.go | 60 ++++++++++- pkg/utils/helm/helm_suite_test.go | 7 ++ references/cli/cli.go | 2 +- references/cli/def.go | 2 +- references/cli/install.go | 2 +- references/common/application.go | 2 +- references/plugins/cluster.go | 2 +- 17 files changed, 363 insertions(+), 31 deletions(-) create mode 100644 pkg/apiserver/rest/usecase/testdata/helm/index.yaml create mode 100644 pkg/apiserver/rest/usecase/testdata/helm/mysql-8.8.23.tgz diff --git a/pkg/addon/versioned_registry.go b/pkg/addon/versioned_registry.go index 3817a9fa4..2ed8a02da 100644 --- a/pkg/addon/versioned_registry.go +++ b/pkg/addon/versioned_registry.go @@ -52,7 +52,7 @@ type versionedRegistry struct { } func (i *versionedRegistry) ListAddon() ([]*UIData, error) { - chartIndex, err := i.h.GetIndexInfo(i.url, false) + chartIndex, err := i.h.GetIndexInfo(i.url, false, nil) if err != nil { return nil, err } @@ -107,7 +107,7 @@ func (i *versionedRegistry) resolveAddonListFromIndex(repoName string, index *re } func (i versionedRegistry) loadAddon(ctx context.Context, name, version string) (*WholeAddonPackage, error) { - versions, err := i.h.ListVersions(i.url, name, false) + versions, err := i.h.ListVersions(i.url, name, false, nil) if err != nil { return nil, err } @@ -131,7 +131,7 @@ func (i versionedRegistry) loadAddon(ctx context.Context, name, version string) return nil, fmt.Errorf("specified version %s not exist", version) } for _, chartURL := range addonVersion.URLs { - archive, err := common.HTTPGet(ctx, chartURL) + archive, err := common.HTTPGetWithOption(ctx, chartURL, nil) if err != nil { continue } diff --git a/pkg/apiserver/rest/usecase/helm.go b/pkg/apiserver/rest/usecase/helm.go index 84ef0c704..8b616df04 100644 --- a/pkg/apiserver/rest/usecase/helm.go +++ b/pkg/apiserver/rest/usecase/helm.go @@ -26,10 +26,12 @@ import ( v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/utils/common" "github.com/oam-dev/kubevela/pkg/utils/helm" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types2 "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "helm.sh/helm/v3/pkg/repo" @@ -61,8 +63,15 @@ type defaultHelmHandler struct { } func (d defaultHelmHandler) ListChartNames(ctx context.Context, url string, secretName string, skipCache bool) ([]string, error) { - // TODO(wangyikewxgm): support authority helm repo - charts, err := d.helper.ListChartsFromRepo(url, skipCache) + var opts *common.HTTPOption + var err error + if len(secretName) != 0 { + opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName}) + if err != nil { + return nil, bcode.ErrRepoBasicAuth + } + } + charts, err := d.helper.ListChartsFromRepo(url, skipCache, opts) if err != nil { log.Logger.Errorf("cannot fetch charts repo: %s, error: %s", url, err.Error()) return nil, bcode.ErrListHelmChart @@ -71,7 +80,15 @@ func (d defaultHelmHandler) ListChartNames(ctx context.Context, url string, secr } func (d defaultHelmHandler) ListChartVersions(ctx context.Context, url string, chartName string, secretName string, skipCache bool) (repo.ChartVersions, error) { - chartVersions, err := d.helper.ListVersions(url, chartName, skipCache) + var opts *common.HTTPOption + var err error + if len(secretName) != 0 { + opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName}) + if err != nil { + return nil, bcode.ErrRepoBasicAuth + } + } + chartVersions, err := d.helper.ListVersions(url, chartName, skipCache, opts) if err != nil { log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s error: %s", url, chartName, err.Error()) return nil, bcode.ErrListHelmVersions @@ -84,7 +101,15 @@ func (d defaultHelmHandler) ListChartVersions(ctx context.Context, url string, c } func (d defaultHelmHandler) GetChartValues(ctx context.Context, url string, chartName string, version string, secretName string, skipCache bool) (map[string]interface{}, error) { - v, err := d.helper.GetValuesFromChart(url, chartName, version, skipCache) + var opts *common.HTTPOption + var err error + if len(secretName) != 0 { + opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName}) + if err != nil { + return nil, bcode.ErrRepoBasicAuth + } + } + v, err := d.helper.GetValuesFromChart(url, chartName, version, skipCache, opts) if err != nil { log.Logger.Errorf("cannot fetch chart values repo: %s, chart: %s, version: %s, error: %s", url, chartName, version, err.Error()) return nil, bcode.ErrGetChartValues diff --git a/pkg/apiserver/rest/usecase/helm_test.go b/pkg/apiserver/rest/usecase/helm_test.go index fd6701fa6..bdad8d153 100644 --- a/pkg/apiserver/rest/usecase/helm_test.go +++ b/pkg/apiserver/rest/usecase/helm_test.go @@ -19,6 +19,10 @@ package usecase import ( "context" "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" "testing" . "github.com/onsi/ginkgo" @@ -104,6 +108,90 @@ var _ = Describe("Test helm repo list", func() { }) }) +var _ = Describe("test helm usecasae", func() { + ctx := context.Background() + var repoSec v1.Secret + + BeforeEach(func() { + Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{})) + + repoSec = v1.Secret{} + Expect(yaml.Unmarshal([]byte(repoSecret), &repoSec)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &repoSec)).Should(BeNil()) + }) + + AfterEach(func() { + Expect(k8sClient.Delete(ctx, &repoSec)).Should(BeNil()) + }) + + It("helm associated usecase interface test", func() { + var mockServer *httptest.Server + + handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + u, p, ok := request.BasicAuth() + if !ok || u != "admin" || p != "admin" { + writer.WriteHeader(401) + return + } + switch { + case request.URL.Path == "/index.yaml": + index, err := ioutil.ReadFile("./testdata/helm/index.yaml") + indexFile := string(index) + indexFile = strings.ReplaceAll(indexFile, "server-url", mockServer.URL) + if err != nil { + writer.Write([]byte(err.Error())) + return + } + writer.Write([]byte(indexFile)) + + return + case strings.Contains(request.URL.Path, "mysql-8.8.23.tgz"): + pkg, err := ioutil.ReadFile("./testdata/helm/mysql-8.8.23.tgz") + if err != nil { + writer.Write([]byte(err.Error())) + return + } + writer.Write(pkg) + return + default: + writer.Write([]byte("404 page not found")) + } + }) + + mockServer = httptest.NewServer(handler) + + defer mockServer.Close() + + u := NewHelmUsecase() + charts, err := u.ListChartNames(ctx, mockServer.URL, "repo-secret", false) + Expect(err).Should(BeNil()) + Expect(len(charts)).Should(BeEquivalentTo(1)) + Expect(charts[0]).Should(BeEquivalentTo("mysql")) + + versions, err := u.ListChartVersions(ctx, mockServer.URL, "mysql", "repo-secret", false) + Expect(err).Should(BeNil()) + Expect(len(versions)).Should(BeEquivalentTo(1)) + Expect(versions[0].Version).Should(BeEquivalentTo("8.8.23")) + + values, err := u.GetChartValues(ctx, mockServer.URL, "mysql", "8.8.23", "repo-secret", false) + Expect(err).Should(BeNil()) + Expect(values).ShouldNot(BeNil()) + Expect(len(values)).ShouldNot(BeEquivalentTo(0)) + }) + + It("coverage not secret notExist error", func() { + u := NewHelmUsecase() + _, err := u.ListChartNames(ctx, "http://127.0.0.1:8080", "repo-secret-notExist", false) + Expect(err).ShouldNot(BeNil()) + + _, err = u.ListChartVersions(ctx, "http://127.0.0.1:8080", "mysql", "repo-secret-notExist", false) + Expect(err).ShouldNot(BeNil()) + + _, err = u.GetChartValues(ctx, "http://127.0.0.1:8080", "mysql", "8.8.23", "repo-secret-notExist", false) + Expect(err).ShouldNot(BeNil()) + }) +}) + var ( src = `{ "OAMSpecVer":"v0.2", @@ -266,5 +354,19 @@ metadata: stringData: url: https://kedacore.github.io/charts type: Opaque +` + repoSecret = ` +apiVersion: v1 +kind: Secret +metadata: + name: repo-secret + namespace: vela-system + labels: + config.oam.dev/type: config-helm-repository + config.oam.dev/project: my-project-2 +stringData: + username: admin + password: admin +type: Opaque ` ) diff --git a/pkg/apiserver/rest/usecase/testdata/helm/index.yaml b/pkg/apiserver/rest/usecase/testdata/helm/index.yaml new file mode 100644 index 000000000..5f70cc885 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/helm/index.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +entries: + mysql: + - annotations: + category: Database + apiVersion: v2 + appVersion: 8.0.28 + created: "2022-04-07T03:26:37.378966939Z" + dependencies: + - name: common + repository: https://charts.bitnami.com/bitnami + tags: + - bitnami-common + version: 1.x.x + description: Chart to create a Highly available MySQL cluster + digest: 96f79c6daba90fb40fc698979fab33f7a60987b1d23cd5080bc885129568a423 + home: https://github.com/bitnami/charts/tree/master/bitnami/mysql + icon: https://bitnami.com/assets/stacks/mysql/img/mysql-stack-220x234.png + keywords: + - mysql + - database + - sql + - cluster + - high availability + maintainers: + - email: containers@bitnami.com + name: Bitnami + name: mysql + sources: + - https://github.com/bitnami/bitnami-docker-mysql + - https://mysql.com + urls: + - server-url/mysql-8.8.23.tgz + version: 8.8.23 +generated: "2022-04-07T03:26:37Z" +serverInfo: {} \ No newline at end of file diff --git a/pkg/apiserver/rest/usecase/testdata/helm/mysql-8.8.23.tgz b/pkg/apiserver/rest/usecase/testdata/helm/mysql-8.8.23.tgz new file mode 100644 index 0000000000000000000000000000000000000000..45c50310e705a08de30016fe6b9ddf4dcbe48384 GIT binary patch literal 39646 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

jxM}XxB|6U}`A(c?^Mwccw`2S87A)NoOw`=$Q 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 literal 0 HcmV?d00001 diff --git a/pkg/apiserver/rest/utils/bcode/013_repository.go b/pkg/apiserver/rest/utils/bcode/013_repository.go index 383bdd1ce..3727a2ed0 100644 --- a/pkg/apiserver/rest/utils/bcode/013_repository.go +++ b/pkg/apiserver/rest/utils/bcode/013_repository.go @@ -30,3 +30,6 @@ var ErrChartNotExist = NewBcode(200, 13004, "this chart not exist in the reposit // ErrSkipCacheParameter means the skip cache parameter miss config var ErrSkipCacheParameter = NewBcode(400, 13005, "skip cache parameter miss config, the value only can be true or false") + +// ErrRepoBasicAuth means extract repo auth info from secret error +var ErrRepoBasicAuth = NewBcode(400, 13006, "extract repo auth info from secret error") diff --git a/pkg/utils/common/common.go b/pkg/utils/common/common.go index def6c7443..602216ebb 100644 --- a/pkg/utils/common/common.go +++ b/pkg/utils/common/common.go @@ -102,6 +102,12 @@ func init() { // +kubebuilder:scaffold:scheme } +// HTTPOption define the https options +type HTTPOption struct { + Username string + Password string +} + // InitBaseRestConfig will return reset config for create controller runtime client func InitBaseRestConfig() (Args, error) { args := Args{ @@ -134,13 +140,16 @@ func GetClient() (client.Client, error) { return nil, errors.New("client not set, call SetGlobalClient first") } -// HTTPGet will send GET http request with context -func HTTPGet(ctx context.Context, url string) ([]byte, error) { +// HTTPGetWithOption use HTTP option and default client to send get request +func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) { // Change NewRequest to NewRequestWithContext and pass context it req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } + if opts != nil && len(opts.Username) != 0 && len(opts.Password) != 0 { + req.SetBasicAuth(opts.Username, opts.Password) + } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err diff --git a/pkg/utils/common/common_test.go b/pkg/utils/common/common_test.go index aa2ea69c5..4439e6b5b 100644 --- a/pkg/utils/common/common_test.go +++ b/pkg/utils/common/common_test.go @@ -75,7 +75,7 @@ func TestHTTPGet(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - got, err := HTTPGet(ctx, tc.url) + got, err := HTTPGetWithOption(ctx, tc.url, nil) if tc.want.errStr != "" { if diff := cmp.Diff(tc.want.errStr, err.Error(), test.EquateErrors()); diff != "" { t.Errorf("\n%s\nHTTPGet(...): -want error, +got error:\n%s", tc.reason, diff) @@ -90,6 +90,91 @@ func TestHTTPGet(t *testing.T) { } +func TestHTTPGetWithOption(t *testing.T) { + type want struct { + data string + } + var ctx = context.Background() + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + u, p, ok := r.BasicAuth() + if !ok { + w.Write([]byte("Error parsing basic auth")) + w.WriteHeader(401) + return + } + if u != "test-user" { + w.Write([]byte(fmt.Sprintf("Username provided is incorrect: %s", u))) + w.WriteHeader(401) + return + } + if p != "test-pass" { + w.Write([]byte(fmt.Sprintf("Password provided is incorrect: %s", p))) + w.WriteHeader(401) + return + } + w.Write([]byte("correct password")) + w.WriteHeader(200) + })) + defer testServer.Close() + + cases := map[string]struct { + opts *HTTPOption + url string + want want + }{ + "without auth case": { + opts: nil, + url: testServer.URL, + want: want{ + data: "Error parsing basic auth", + }, + }, + "error user name case": { + opts: &HTTPOption{ + Username: "no-user", + Password: "test-pass", + }, + url: testServer.URL, + want: want{ + data: "Username provided is incorrect: no-user", + }, + }, + "error password case": { + opts: &HTTPOption{ + Username: "test-user", + Password: "error-pass", + }, + url: testServer.URL, + want: want{ + data: "Password provided is incorrect: error-pass", + }, + }, + "correct password case": { + opts: &HTTPOption{ + Username: "test-user", + Password: "test-pass", + }, + url: testServer.URL, + want: want{ + data: "correct password", + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, err := HTTPGetWithOption(ctx, tc.url, tc.opts) + assert.NoError(t, err) + + if diff := cmp.Diff(tc.want.data, string(got)); diff != "" { + t.Errorf("\n%s\nHTTPGet(...): -want, +got:\n%s", tc.want.data, diff) + } + }) + } + +} + func TestGetCUEParameterValue(t *testing.T) { type want struct { err error diff --git a/pkg/utils/helm/helm.go b/pkg/utils/helm/helm.go index 698282a54..2efda7940 100644 --- a/pkg/utils/helm/helm.go +++ b/pkg/utils/helm/helm.go @@ -17,12 +17,17 @@ limitations under the License. package helm import ( + "context" "fmt" "io" "log" "os" "strings" + v1 "k8s.io/api/core/v1" + types2 "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/pkg/errors" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" @@ -268,3 +273,13 @@ func GetChart(client *action.Install, name string) (*chart.Chart, error) { func InstallHelmChart(ioStreams cmdutil.IOStreams, c types.Chart) error { return Install(ioStreams, c.Repo, c.URL, c.Name, c.Version, c.Namespace, c.Name, c.Values) } + +// SetBasicAuthInfo will read username and password from secret return a httpOption that contain these info. +func SetBasicAuthInfo(ctx context.Context, k8sClient client.Client, secretRef types2.NamespacedName) (*common.HTTPOption, error) { + sec := v1.Secret{} + err := k8sClient.Get(ctx, secretRef, &sec) + if err != nil { + return nil, err + } + return &common.HTTPOption{Username: string(sec.Data["username"]), Password: string(sec.Data["password"])}, nil +} diff --git a/pkg/utils/helm/helm_helper.go b/pkg/utils/helm/helm_helper.go index 2953be6b0..fdfacf669 100644 --- a/pkg/utils/helm/helm_helper.go +++ b/pkg/utils/helm/helm_helper.go @@ -75,11 +75,11 @@ func NewHelperWithCache() *Helper { } // LoadCharts load helm chart from local or remote -func (h *Helper) LoadCharts(chartRepoURL string) (*chart.Chart, error) { +func (h *Helper) LoadCharts(chartRepoURL string, opts *common.HTTPOption) (*chart.Chart, error) { var err error var chart *chart.Chart if utils.IsValidURL(chartRepoURL) { - chartBytes, err := common.HTTPGet(context.Background(), chartRepoURL) + chartBytes, err := common.HTTPGetWithOption(context.Background(), chartRepoURL, opts) if err != nil { return nil, errors.New("error retrieving Helm Chart at " + chartRepoURL + ": " + err.Error()) } @@ -194,8 +194,8 @@ func (h *Helper) UninstallRelease(releaseName, namespace string, config *rest.Co } // ListVersions list available versions from repo -func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool) (repo.ChartVersions, error) { - i, err := h.GetIndexInfo(repoURL, skipCache) +func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool, opts *common.HTTPOption) (repo.ChartVersions, error) { + i, err := h.GetIndexInfo(repoURL, skipCache, opts) if err != nil { return nil, err } @@ -203,7 +203,7 @@ func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool) } // GetIndexInfo get index.yaml form given repo url -func (h *Helper) GetIndexInfo(repoURL string, skipCache bool) (*repo.IndexFile, error) { +func (h *Helper) GetIndexInfo(repoURL string, skipCache bool, opts *common.HTTPOption) (*repo.IndexFile, error) { if h.cache != nil && !skipCache { if i := h.cache.Get(fmt.Sprintf(repoPatten, repoURL)); i != nil { return i.(*repo.IndexFile), nil @@ -218,7 +218,7 @@ func (h *Helper) GetIndexInfo(repoURL string, skipCache bool) (*repo.IndexFile, parsedURL.RawPath = path.Join(parsedURL.RawPath, "index.yaml") parsedURL.Path = path.Join(parsedURL.Path, "index.yaml") indexURL := parsedURL.String() - body, err = common.HTTPGet(context.Background(), indexURL) + body, err = common.HTTPGetWithOption(context.Background(), indexURL, opts) if err != nil { return nil, fmt.Errorf("download index file from %s failure %w", repoURL, err) } @@ -300,8 +300,8 @@ func newActionConfig(config *rest.Config, namespace string, showDetail bool, log } // ListChartsFromRepo list available helm charts in a repo -func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool) ([]string, error) { - i, err := h.GetIndexInfo(repoURL, skipCache) +func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool, opts *common.HTTPOption) ([]string, error) { + i, err := h.GetIndexInfo(repoURL, skipCache, opts) if err != nil { return nil, err } @@ -315,13 +315,13 @@ func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool) ([]string, e } // GetValuesFromChart will extract the parameter from a helm chart -func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version string, skipCache bool) (map[string]interface{}, error) { +func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version string, skipCache bool, opts *common.HTTPOption) (map[string]interface{}, error) { if h.cache != nil && !skipCache { if v := h.cache.Get(fmt.Sprintf(valuesPatten, repoURL, chartName, version)); v != nil { return v.(map[string]interface{}), nil } } - i, err := h.GetIndexInfo(repoURL, skipCache) + i, err := h.GetIndexInfo(repoURL, skipCache, opts) if err != nil { return nil, err } @@ -336,7 +336,7 @@ func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version st } } for _, u := range urls { - c, err := h.LoadCharts(u) + c, err := h.LoadCharts(u, opts) if err != nil { continue } diff --git a/pkg/utils/helm/helm_helper_test.go b/pkg/utils/helm/helm_helper_test.go index 7fa62e424..f32f4142f 100644 --- a/pkg/utils/helm/helm_helper_test.go +++ b/pkg/utils/helm/helm_helper_test.go @@ -17,12 +17,20 @@ limitations under the License. package helm import ( + "context" "os" - "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/google/go-cmp/cmp" + v1 "k8s.io/api/core/v1" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/yaml" + + types2 "github.com/oam-dev/kubevela/apis/types" + util2 "github.com/oam-dev/kubevela/pkg/oam/util" "github.com/oam-dev/kubevela/pkg/utils/util" ) @@ -30,7 +38,7 @@ var _ = Describe("Test helm helper", func() { It("Test LoadCharts ", func() { helper := NewHelper() - chart, err := helper.LoadCharts("./testdata/autoscalertrait-0.1.0.tgz") + chart, err := helper.LoadCharts("./testdata/autoscalertrait-0.1.0.tgz", nil) Expect(err).Should(BeNil()) Expect(chart).ShouldNot(BeNil()) Expect(chart.Metadata).ShouldNot(BeNil()) @@ -39,7 +47,7 @@ var _ = Describe("Test helm helper", func() { It("Test UpgradeChart", func() { helper := NewHelper() - chart, err := helper.LoadCharts("./testdata/autoscalertrait-0.1.0.tgz") + chart, err := helper.LoadCharts("./testdata/autoscalertrait-0.1.0.tgz", nil) Expect(err).Should(BeNil()) release, err := helper.UpgradeChart(chart, "autoscalertrait", "default", nil, UpgradeChartOptions{ Config: cfg, @@ -60,15 +68,57 @@ var _ = Describe("Test helm helper", func() { It("Test ListVersions ", func() { helper := NewHelper() - versions, err := helper.ListVersions("./testdata", "autoscalertrait", true) + versions, err := helper.ListVersions("./testdata", "autoscalertrait", true, nil) Expect(err).Should(BeNil()) Expect(cmp.Diff(len(versions), 2)).Should(BeEmpty()) }) It("Test getValues from chart", func() { helper := NewHelper() - values, err := helper.GetValuesFromChart("./testdata", "autoscalertrait", "0.2.0", true) + values, err := helper.GetValuesFromChart("./testdata", "autoscalertrait", "0.2.0", true, nil) Expect(err).Should(BeNil()) Expect(values).ShouldNot(BeEmpty()) }) }) + +var _ = Describe("Test helm associated func", func() { + ctx := context.Background() + var aSec v1.Secret + + BeforeEach(func() { + Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: v12.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util2.AlreadyExistMatcher{})) + aSec = v1.Secret{} + Expect(yaml.Unmarshal([]byte(authSecret), &aSec)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &aSec)).Should(SatisfyAny(BeNil(), util2.AlreadyExistMatcher{})) + }) + + It("Test auth info secret func", func() { + opts, err := SetBasicAuthInfo(context.Background(), k8sClient, types.NamespacedName{Namespace: types2.DefaultKubeVelaNS, Name: "auth-secret"}) + Expect(err).Should(BeNil()) + Expect(opts.Username).Should(BeEquivalentTo("admin")) + Expect(opts.Password).Should(BeEquivalentTo("admin")) + }) + + It("Test auth info secret func", func() { + _, err := SetBasicAuthInfo(context.Background(), k8sClient, types.NamespacedName{Namespace: types2.DefaultKubeVelaNS, Name: "auth-secret-1"}) + Expect(err).ShouldNot(BeNil()) + }) +}) + +var ( + authSecret = ` +apiVersion: v1 +kind: Secret +metadata: + name: auth-secret + namespace: vela-system + labels: + config.oam.dev/type: config-helm-repository + config.oam.dev/project: my-project-1 +stringData: + url: https://kedacore.github.io/charts + username: admin + password: admin +type: Opaque +` +) diff --git a/pkg/utils/helm/helm_suite_test.go b/pkg/utils/helm/helm_suite_test.go index 2f6d0b274..3b80da9b7 100644 --- a/pkg/utils/helm/helm_suite_test.go +++ b/pkg/utils/helm/helm_suite_test.go @@ -21,14 +21,18 @@ import ( "testing" "time" + "github.com/oam-dev/kubevela/pkg/utils/common" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/client-go/rest" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" ) var cfg *rest.Config +var k8sClient client.Client var testEnv *envtest.Environment var _ = BeforeSuite(func(done Done) { @@ -47,6 +51,9 @@ var _ = BeforeSuite(func(done Done) { Expect(err).ShouldNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) + k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) close(done) }, 240) diff --git a/references/cli/cli.go b/references/cli/cli.go index de7b38390..496646fc6 100644 --- a/references/cli/cli.go +++ b/references/cli/cli.go @@ -165,7 +165,7 @@ func NewVersionListCommand(ioStream util.IOStreams) *cobra.Command { Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { helmHelper := helm.NewHelper() - versions, err := helmHelper.ListVersions(kubevelaInstallerHelmRepoURL, kubeVelaChartName, true) + versions, err := helmHelper.ListVersions(kubevelaInstallerHelmRepoURL, kubeVelaChartName, true, nil) if err != nil { return err } diff --git a/references/cli/def.go b/references/cli/def.go index e01d9eb12..025d5a324 100644 --- a/references/cli/def.go +++ b/references/cli/def.go @@ -108,7 +108,7 @@ func getPrompt(cmd *cobra.Command, reader *bufio.Reader, description string, pro func loadYAMLBytesFromFileOrHTTP(pathOrURL string) ([]byte, error) { if strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://") { - return common.HTTPGet(context.Background(), pathOrURL) + return common.HTTPGetWithOption(context.Background(), pathOrURL, nil) } return os.ReadFile(path.Clean(pathOrURL)) } diff --git a/references/cli/install.go b/references/cli/install.go index d4a360305..f2f4ed0e5 100644 --- a/references/cli/install.go +++ b/references/cli/install.go @@ -106,7 +106,7 @@ func NewInstallCommand(c common.Args, order string, ioStreams util.IOStreams) *c if installArgs.ChartFilePath == "" { installArgs.ChartFilePath = getKubeVelaHelmChartRepoURL(installArgs.Version) } - chart, err := installArgs.helmHelper.LoadCharts(installArgs.ChartFilePath) + chart, err := installArgs.helmHelper.LoadCharts(installArgs.ChartFilePath, nil) if err != nil { return fmt.Errorf("loadding the helm chart of kubeVela control plane failure, %w", err) } diff --git a/references/common/application.go b/references/common/application.go index 7c5003452..a629af8f6 100644 --- a/references/common/application.go +++ b/references/common/application.go @@ -257,7 +257,7 @@ func ReadRemoteOrLocalPath(pathOrURL string) ([]byte, error) { var body []byte var err error if utils.IsValidURL(pathOrURL) { - body, err = common.HTTPGet(context.Background(), pathOrURL) + body, err = common.HTTPGetWithOption(context.Background(), pathOrURL, nil) if err != nil { return nil, err } diff --git a/references/plugins/cluster.go b/references/plugins/cluster.go index 78ee6f556..a1c523ea7 100644 --- a/references/plugins/cluster.go +++ b/references/plugins/cluster.go @@ -270,7 +270,7 @@ func HandleTemplate(in *runtime.RawExtension, schematic *commontypes.Schematic, } } if tmp.CueTemplateURI != "" { - b, err := common.HTTPGet(context.Background(), tmp.CueTemplateURI) + b, err := common.HTTPGetWithOption(context.Background(), tmp.CueTemplateURI, nil) if err != nil { return types.Capability{}, err }