Просмотр исходного кода

add support for displaying helm repos

Alexander Belanger 4 лет назад
Родитель
Сommit
2a06974756

+ 81 - 0
api/server/handlers/helmrepo/get_chart.go

@@ -0,0 +1,81 @@
+package helmrepo
+
+import (
+	"net/http"
+	"strings"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/handlers/release"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/templater/parser"
+)
+
+type ChartGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewChartGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ChartGetHandler {
+	return &ChartGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *ChartGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	helmRepo, _ := r.Context().Value(types.HelmRepoScope).(*models.HelmRepo)
+
+	name, _ := requestutils.GetURLParamString(r, types.URLParamTemplateName)
+	version, _ := requestutils.GetURLParamString(r, types.URLParamTemplateVersion)
+
+	// if version passed as latest, pass empty string to loader to get latest
+	if version == "latest" {
+		version = ""
+	}
+
+	chart, err := release.LoadChart(t.Config(), &release.LoadAddonChartOpts{
+		ProjectID:       proj.ID,
+		RepoURL:         helmRepo.RepoURL,
+		TemplateName:    name,
+		TemplateVersion: version,
+	})
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	parserDef := &parser.ClientConfigDefault{
+		HelmChart: chart,
+	}
+
+	res := &types.GetTemplateResponse{
+		RepoURL: helmRepo.RepoURL,
+	}
+	res.Metadata = chart.Metadata
+	res.Values = chart.Values
+
+	for _, file := range chart.Files {
+		if strings.Contains(file.Name, "form.yaml") {
+			formYAML, err := parser.FormYAMLFromBytes(parserDef, file.Data, "declared")
+
+			if err != nil {
+				break
+			}
+
+			res.Form = formYAML
+		} else if strings.Contains(file.Name, "README.md") {
+			res.Markdown = string(file.Data)
+		}
+	}
+
+	t.WriteResult(w, r, res)
+}

+ 1 - 1
api/server/handlers/helmrepo/list_charts.go

@@ -57,7 +57,7 @@ func (t *ChartListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	charts := loader.RepoIndexToPorterChartList(repoIndex)
+	charts := loader.RepoIndexToPorterChartList(repoIndex, helmRepo.RepoURL)
 
 	t.WriteResult(w, r, charts)
 }

+ 1 - 1
api/server/handlers/template/list.go

@@ -47,7 +47,7 @@ func (t *TemplateListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	porterCharts := loader.RepoIndexToPorterChartList(repoIndex)
+	porterCharts := loader.RepoIndexToPorterChartList(repoIndex, repoURL)
 
 	t.WriteResult(w, r, porterCharts)
 }

+ 29 - 0
api/server/router/helm_repo.go

@@ -109,5 +109,34 @@ func getHelmRepoRoutes(
 		Router:   r,
 	})
 
+	//  GET /api/projects/{project_id}/helmrepos/{helm_repo_id}/charts/{name}/{version} -> helmrepo.NewChartGetHandler
+	chartGetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/charts/{name}/{version}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.HelmRepoScope,
+			},
+		},
+	)
+
+	chartGetHandler := helmrepo.NewChartGetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: chartGetEndpoint,
+		Handler:  chartGetHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 2 - 0
api/types/template.go

@@ -23,6 +23,7 @@ type PorterTemplateSimple struct {
 	Versions    []string `json:"versions"`
 	Description string   `json:"description"`
 	Icon        string   `json:"icon"`
+	RepoURL     string   `json:"repo_url,omitempty"`
 }
 
 // ListTemplatesResponse is how a chart gets displayed when listed
@@ -38,6 +39,7 @@ type GetTemplateResponse struct {
 	Metadata *chart.Metadata        `json:"metadata"`
 	Values   map[string]interface{} `json:"values"`
 	Form     *FormYAML              `json:"form"`
+	RepoURL  string                 `json:"repo_url,omitempty"`
 }
 
 type GetTemplateUpgradeNotesRequest struct {

+ 66 - 53
dashboard/src/main/home/launch/Launch.tsx

@@ -20,12 +20,19 @@ import { hardcodedNames } from "shared/hardcodedNameDict";
 import semver from "semver";
 import { RouteComponentProps, withRouter } from "react-router";
 import { getQueryParam, getQueryParams } from "shared/routing";
+import TemplateList from "./TemplateList";
+import { capitalize } from "lodash";
 
-const tabOptions = [
+const initialTabOptions = [
   { label: "New Application", value: "porter" },
   { label: "Community Add-ons", value: "community" },
 ];
 
+type TabOption = {
+  label: string;
+  value: string;
+};
+
 const HIDDEN_CHARTS = ["porter-agent"];
 
 type PropsType = RouteComponentProps & {};
@@ -40,8 +47,8 @@ type StateType = {
   error: boolean;
   isOnLaunchFlow: boolean;
   clonedChart: ChartTypeWithExtendedConfig;
+  tabOptions: TabOption[];
 };
-
 class Templates extends Component<PropsType, StateType> {
   state = {
     currentTemplate: null as PorterTemplate | null,
@@ -53,6 +60,7 @@ class Templates extends Component<PropsType, StateType> {
     error: false,
     isOnLaunchFlow: false,
     clonedChart: null as ChartTypeWithExtendedConfig,
+    tabOptions: initialTabOptions,
   };
 
   async componentDidMount() {
@@ -163,6 +171,29 @@ class Templates extends Component<PropsType, StateType> {
     } catch (error) {
       this.setState({ loading: false, error: true });
     }
+
+    try {
+      const res = await api.getHelmRepos(
+        "<token>",
+        {},
+        {
+          project_id: this.context.currentProject.id,
+        }
+      );
+
+      let tabOptions = this.state.tabOptions.concat(
+        ...res.data.map((val: any) => {
+          return {
+            value: `${val.id}`,
+            label: capitalize(val.name),
+          };
+        })
+      );
+
+      this.setState({ tabOptions });
+    } catch (error) {
+      this.setState({ loading: false, error: true });
+    }
   }
 
   isTryingToClone = () => {
@@ -222,48 +253,37 @@ class Templates extends Component<PropsType, StateType> {
     );
   };
 
-  renderTemplateList = (templates: any) => {
-    let { loading, error } = this.state;
-
-    if (loading) {
-      return (
-        <LoadingWrapper>
-          <Loading />
-        </LoadingWrapper>
-      );
-    } else if (error) {
-      return (
-        <Placeholder>
-          <i className="material-icons">error</i> Error retrieving templates.
-        </Placeholder>
-      );
-    } else if (templates.length === 0) {
-      return (
-        <Placeholder>
-          <i className="material-icons">category</i> No templates found.
-        </Placeholder>
-      );
+  renderTemplateList = (templates?: any, helm_repo_id?: number) => {
+    if (!helm_repo_id && templates) {
+      if (this.state.loading) {
+        return (
+          <LoadingWrapper>
+            <Loading />
+          </LoadingWrapper>
+        );
+      } else if (this.state.error) {
+        return (
+          <Placeholder>
+            <i className="material-icons">error</i> Error retrieving templates.
+          </Placeholder>
+        );
+      } else if (templates.length === 0) {
+        return (
+          <Placeholder>
+            <i className="material-icons">category</i> No templates found.
+          </Placeholder>
+        );
+      }
     }
 
     return (
-      <TemplateList>
-        {templates.map((template: PorterTemplate, i: number) => {
-          let { name, icon, description } = template;
-          if (hardcodedNames[name]) {
-            name = hardcodedNames[name];
-          }
-          return (
-            <TemplateBlock
-              key={name}
-              onClick={() => this.setState({ currentTemplate: template })}
-            >
-              {this.renderIcon(icon)}
-              <TemplateTitle>{name}</TemplateTitle>
-              <TemplateDescription>{description}</TemplateDescription>
-            </TemplateBlock>
-          );
-        })}
-      </TemplateList>
+      <TemplateList
+        helm_repo_id={helm_repo_id}
+        templates={templates}
+        setCurrentTemplate={(template) =>
+          this.setState({ currentTemplate: template })
+        }
+      />
     );
   };
 
@@ -278,13 +298,16 @@ class Templates extends Component<PropsType, StateType> {
           setCurrentTemplate={(currentTemplate: PorterTemplate) => {
             this.setState({ currentTemplate });
           }}
+          helm_repo_id={parseInt(this.state.currentTab)}
         />
       );
     }
     if (this.state.currentTab === "porter") {
       return this.renderTemplateList(this.state.applicationTemplates);
-    } else {
+    } else if (this.state.currentTab == "community") {
       return this.renderTemplateList(this.state.addonTemplates);
+    } else {
+      return this.renderTemplateList(null, parseInt(this.state.currentTab));
     }
   };
 
@@ -293,7 +316,7 @@ class Templates extends Component<PropsType, StateType> {
       return (
         <>
           <TabSelector
-            options={tabOptions}
+            options={this.state.tabOptions}
             currentTab={this.state.currentTab}
             setCurrentTab={(value: string) =>
               this.setState({
@@ -489,16 +512,6 @@ const TemplateBlock = styled.div`
   }
 `;
 
-const TemplateList = styled.div`
-  overflow: visible;
-  margin-top: 35px;
-  padding-bottom: 150px;
-  display: grid;
-  grid-column-gap: 25px;
-  grid-row-gap: 25px;
-  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
-`;
-
 const TemplatesWrapper = styled.div`
   width: calc(85%);
   overflow: visible;

+ 284 - 0
dashboard/src/main/home/launch/TemplateList.tsx

@@ -0,0 +1,284 @@
+import React, { useContext, useEffect, useMemo, useState } from "react";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import styled from "styled-components";
+
+import Loading from "components/Loading";
+import { hardcodedNames } from "shared/hardcodedNameDict";
+import { PorterTemplate } from "shared/types";
+import semver from "semver";
+
+type Props = {
+  helm_repo_id?: number;
+  templates?: PorterTemplate[];
+  setCurrentTemplate: (template: PorterTemplate) => void;
+};
+
+const TemplateList: React.FC<Props> = ({
+  helm_repo_id,
+  templates,
+  setCurrentTemplate,
+}) => {
+  const [isLoading, setIsLoading] = useState(!!helm_repo_id);
+  const [hasError, setHasError] = useState(false);
+  const [templateList, setTemplateList] = useState<PorterTemplate[]>(null);
+  const { currentProject, setCurrentError } = useContext(Context);
+
+  useEffect(() => {
+    if (currentProject && helm_repo_id) {
+      let isSubscribed = true;
+
+      api
+        .getChartsFromHelmRepo(
+          "<token>",
+          {},
+          {
+            project_id: currentProject.id,
+            helm_repo_id: helm_repo_id,
+          }
+        )
+        .then(({ data }) => {
+          if (!isSubscribed) {
+            return;
+          }
+
+          if (!Array.isArray(data)) {
+            throw Error("Data is not an array");
+          }
+
+          let sortedVersionData = data.map((template: any) => {
+            let versions = template.versions.reverse();
+
+            versions = template.versions.sort(semver.rcompare);
+
+            return {
+              ...template,
+              versions,
+              currentVersion: versions[0],
+            };
+          });
+          sortedVersionData.sort((a: any, b: any) =>
+            a.name > b.name ? 1 : -1
+          );
+
+          setTemplateList(sortedVersionData);
+          setIsLoading(false);
+        })
+        .catch((err) => {
+          console.error(err);
+
+          setHasError(true);
+          setCurrentError(err.response?.data?.error);
+          setIsLoading(false);
+        });
+
+      return () => {
+        isSubscribed = false;
+      };
+    }
+  }, [currentProject, helm_repo_id]);
+
+  if (isLoading || (!templates && !templateList)) {
+    return (
+      <LoadingWrapper>
+        <Loading />
+      </LoadingWrapper>
+    );
+  } else if (hasError) {
+    return (
+      <Placeholder>
+        <i className="material-icons">error</i> Error retrieving templates.
+      </Placeholder>
+    );
+  } else if (templateList && templateList.length === 0) {
+    return (
+      <Placeholder>
+        <i className="material-icons">category</i> No templates found.
+      </Placeholder>
+    );
+  }
+
+  const renderIcon = (icon: string) => {
+    if (icon) {
+      return <Icon src={icon} />;
+    }
+
+    return (
+      <Polymer>
+        <i className="material-icons">layers</i>
+      </Polymer>
+    );
+  };
+
+  return (
+    <TemplateListWrapper>
+      {(templates || templateList)?.map(
+        (template: PorterTemplate, i: number) => {
+          let { name, icon, description } = template;
+          if (hardcodedNames[name]) {
+            name = hardcodedNames[name];
+          }
+          return (
+            <TemplateBlock
+              key={name}
+              onClick={() => setCurrentTemplate(template)}
+            >
+              {renderIcon(icon)}
+              <TemplateTitle>{name}</TemplateTitle>
+              <TemplateDescription>{description}</TemplateDescription>
+            </TemplateBlock>
+          );
+        }
+      )}
+    </TemplateListWrapper>
+  );
+};
+
+export default TemplateList;
+
+const Placeholder = styled.div`
+  padding-top: 200px;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #ffffff44;
+  font-size: 14px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 12px;
+  }
+`;
+
+const Banner = styled.div`
+  height: 40px;
+  width: 100%;
+  margin: 30px 0 38px;
+  font-size: 13px;
+  display: flex;
+  border-radius: 5px;
+  padding-left: 15px;
+  align-items: center;
+  background: #ffffff11;
+  > i {
+    margin-right: 10px;
+    font-size: 18px;
+  }
+`;
+
+const Highlight = styled.div`
+  color: #8590ff;
+  cursor: pointer;
+  margin-left: 5px;
+  margin-right: 10px;
+`;
+
+const StyledStatusPlaceholder = styled.div`
+  width: 100%;
+  height: calc(100vh - 365px);
+  margin-top: 20px;
+  display: flex;
+  color: #aaaabb;
+  border-radius: 5px;
+  padding-bottom: 20px;
+  text-align: center;
+  font-size: 13px;
+  background: #ffffff09;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-family: "Work Sans", sans-serif;
+  user-select: text;
+`;
+
+const LoadingWrapper = styled.div`
+  padding-top: 300px;
+`;
+
+const Icon = styled.img`
+  height: 42px;
+  margin-top: 35px;
+  margin-bottom: 13px;
+`;
+
+const Polymer = styled.div`
+  > i {
+    font-size: 34px;
+    margin-top: 38px;
+    margin-bottom: 20px;
+  }
+`;
+
+const TemplateDescription = styled.div`
+  margin-bottom: 26px;
+  color: #ffffff66;
+  text-align: center;
+  font-weight: default;
+  padding: 0px 25px;
+  height: 2.4em;
+  font-size: 12px;
+  display: -webkit-box;
+  overflow: hidden;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+`;
+
+const TemplateTitle = styled.div`
+  margin-bottom: 12px;
+  width: 80%;
+  text-align: center;
+  font-size: 14px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const TemplateBlock = styled.div`
+  border: 1px solid #ffffff00;
+  align-items: center;
+  user-select: none;
+  border-radius: 8px;
+  display: flex;
+  font-size: 13px;
+  font-weight: 500;
+  padding: 3px 0px 5px;
+  flex-direction: column;
+  align-item: center;
+  justify-content: space-between;
+  height: 200px;
+  cursor: pointer;
+  color: #ffffff;
+  position: relative;
+  background: #26282f;
+  box-shadow: 0 4px 15px 0px #00000044;
+  :hover {
+    background: #ffffff11;
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const TemplateListWrapper = styled.div`
+  overflow: visible;
+  margin-top: 35px;
+  padding-bottom: 150px;
+  display: grid;
+  grid-column-gap: 25px;
+  grid-row-gap: 25px;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+`;
+
+const TemplatesWrapper = styled.div`
+  width: calc(85%);
+  overflow: visible;
+  min-width: 300px;
+`;

+ 56 - 23
dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx

@@ -6,6 +6,7 @@ import api from "shared/api";
 
 import TemplateInfo from "./TemplateInfo";
 import Loading from "components/Loading";
+import { Context } from "shared/Context";
 
 type PropsType = {
   currentTemplate: PorterTemplate;
@@ -14,6 +15,8 @@ type PropsType = {
   skipDescription?: boolean;
   showLaunchFlow: () => void;
   setForm: (x: any) => void;
+  helm_repo_id?: number;
+  repo_url?: string;
 };
 
 type StateType = {
@@ -43,29 +46,57 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
 
   fetchTemplateInfo = () => {
     this.setState({ loading: true });
-    let params =
-      this.props.currentTab == "porter"
-        ? { repo_url: process.env.APPLICATION_CHART_REPO_URL }
-        : { repo_url: process.env.ADDON_CHART_REPO_URL };
-
-    api
-      .getTemplateInfo("<token>", params, {
-        name: this.props.currentTemplate.name.toLowerCase().trim(),
-        version: this.props.currentTemplate.currentVersion,
-      })
-      .then((res) => {
-        let { form, values, markdown, metadata } = res.data;
-        let keywords = metadata.keywords;
-        this.props.setForm(form);
-        this.setState({
-          values,
-          markdown,
-          keywords,
-          loading: false,
-          error: false,
-        });
-      })
-      .catch((err) => this.setState({ loading: false, error: true }));
+
+    if (this.props.helm_repo_id) {
+      api
+        .getChartInfoFromHelmRepo(
+          "<token>",
+          {},
+          {
+            project_id: this.context.currentProject.id,
+            helm_repo_id: this.props.helm_repo_id,
+            name: this.props.currentTemplate.name.toLowerCase().trim(),
+            version: this.props.currentTemplate.currentVersion,
+          }
+        )
+        .then((res) => {
+          let { form, values, markdown, metadata } = res.data;
+          let keywords = metadata.keywords;
+          this.props.setForm(form);
+          this.setState({
+            values,
+            markdown,
+            keywords,
+            loading: false,
+            error: false,
+          });
+        })
+        .catch((err) => this.setState({ loading: false, error: true }));
+    } else {
+      let params =
+        this.props.currentTab == "porter"
+          ? { repo_url: process.env.APPLICATION_CHART_REPO_URL }
+          : { repo_url: process.env.ADDON_CHART_REPO_URL };
+
+      api
+        .getTemplateInfo("<token>", params, {
+          name: this.props.currentTemplate.name.toLowerCase().trim(),
+          version: this.props.currentTemplate.currentVersion,
+        })
+        .then((res) => {
+          let { form, values, markdown, metadata } = res.data;
+          let keywords = metadata.keywords;
+          this.props.setForm(form);
+          this.setState({
+            values,
+            markdown,
+            keywords,
+            loading: false,
+            error: false,
+          });
+        })
+        .catch((err) => this.setState({ loading: false, error: true }));
+    }
   };
 
   componentDidUpdate = (prevProps: PropsType) => {
@@ -116,6 +147,8 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
   }
 }
 
+ExpandedTemplate.contextType = Context;
+
 const FadeWrapper = styled.div`
   animation: fadeIn 0.2s;
   @keyframes fadeIn {

+ 2 - 1
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -115,7 +115,8 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
           id: currentProject.id,
           cluster_id: currentCluster.id,
           namespace: selectedNamespace,
-          repo_url: process.env.ADDON_CHART_REPO_URL,
+          repo_url:
+            props.currentTemplate?.repo_url || process.env.ADDON_CHART_REPO_URL,
         }
       )
       .then((_) => {

+ 29 - 0
dashboard/src/shared/api.tsx

@@ -931,6 +931,32 @@ const getTemplates = baseApi<
   {}
 >("GET", "/api/templates");
 
+const getHelmRepos = baseApi<
+  {},
+  {
+    project_id: number;
+  }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/helmrepos`;
+});
+
+const getChartsFromHelmRepo = baseApi<
+  {},
+  {
+    project_id: number;
+    helm_repo_id: number;
+  }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/helmrepos/${pathParams.helm_repo_id}/charts`;
+});
+
+const getChartInfoFromHelmRepo = baseApi<
+  {},
+  { project_id: number; helm_repo_id: number; name: string; version: string }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/helmrepos/${pathParams.helm_repo_id}/charts/${pathParams.name}/${pathParams.version}`;
+});
+
 const getMetadata = baseApi<{}, {}>("GET", () => {
   return `/api/metadata`;
 });
@@ -1563,6 +1589,9 @@ export default {
   getTemplateInfo,
   getTemplateUpgradeNotes,
   getTemplates,
+  getHelmRepos,
+  getChartsFromHelmRepo,
+  getChartInfoFromHelmRepo,
   linkGithubProject,
   getGithubAccounts,
   listConfigMaps,

+ 1 - 0
dashboard/src/shared/types.tsx

@@ -152,6 +152,7 @@ export interface PorterTemplate {
   currentVersion: string;
   description: string;
   icon: string;
+  repo_url?: string;
 }
 
 // FormYAML represents a chart's values.yaml form abstraction

+ 2 - 1
internal/helm/loader/loader.go

@@ -17,7 +17,7 @@ import (
 )
 
 // RepoIndexToPorterChartList converts an index file to a list of porter charts
-func RepoIndexToPorterChartList(index *repo.IndexFile) types.ListTemplatesResponse {
+func RepoIndexToPorterChartList(index *repo.IndexFile, repoURL string) types.ListTemplatesResponse {
 	// sort the entries before parsing
 	index.SortEntries()
 
@@ -36,6 +36,7 @@ func RepoIndexToPorterChartList(index *repo.IndexFile) types.ListTemplatesRespon
 			Description: indexChart.Description,
 			Icon:        indexChart.Icon,
 			Versions:    versions,
+			RepoURL:     repoURL,
 		}
 
 		porterCharts = append(porterCharts, porterChart)

+ 1 - 1
internal/helm/repo/repo.go

@@ -59,7 +59,7 @@ func (hr *HelmRepo) listChartsBasic(
 		return nil, err
 	}
 
-	return loader.RepoIndexToPorterChartList(repoIndex), nil
+	return loader.RepoIndexToPorterChartList(repoIndex, hr.RepoURL), nil
 }
 
 func (hr *HelmRepo) getChartBasic(