Selaa lähdekoodia

Merge pull request #2551 from porter-dev/belanger/remove-repo-url-dependency

Change all template paths to be project-scoped
abelanger5 3 vuotta sitten
vanhempi
sitoutus
5e9093fb08

+ 5 - 2
api/client/template.go

@@ -9,13 +9,14 @@ import (
 
 func (c *Client) ListTemplates(
 	ctx context.Context,
+	projectID uint,
 	req *types.ListTemplatesRequest,
 ) (*types.ListTemplatesResponse, error) {
 	resp := &types.ListTemplatesResponse{}
 
 	err := c.getRequest(
 		fmt.Sprintf(
-			"/templates",
+			"/v1/projects/%d/templates", projectID,
 		),
 		req,
 		resp,
@@ -26,6 +27,7 @@ func (c *Client) ListTemplates(
 
 func (c *Client) GetTemplate(
 	ctx context.Context,
+	projectID uint,
 	name, version string,
 	req *types.GetTemplateRequest,
 ) (*types.GetTemplateResponse, error) {
@@ -33,7 +35,8 @@ func (c *Client) GetTemplate(
 
 	err := c.getRequest(
 		fmt.Sprintf(
-			"/templates/%s/%s",
+			"/v1/projects/%d/templates/%s/versions/%s",
+			projectID,
 			name, version,
 		),
 		req,

+ 23 - 0
api/server/handlers/release/create.go

@@ -19,6 +19,7 @@ import (
 	"github.com/porter-dev/porter/internal/encryption"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/helm/repo"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/integrations/ci/gitlab"
 	"github.com/porter-dev/porter/internal/models"
@@ -76,6 +77,28 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		request.RepoURL = c.Config().ServerConf.DefaultApplicationHelmRepoURL
 	}
 
+	// if the repo url is not an addon or application url, validate against the helm repos
+	if request.RepoURL != c.Config().ServerConf.DefaultAddonHelmRepoURL && request.RepoURL != c.Config().ServerConf.DefaultApplicationHelmRepoURL {
+		// load the helm repos in the project
+		hrs, err := c.Repo().HelmRepo().ListHelmReposByProjectID(cluster.ProjectID)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		isValid := repo.ValidateRepoURL(c.Config().ServerConf.DefaultAddonHelmRepoURL, c.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, request.RepoURL)
+
+		if !isValid {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("invalid repo_url parameter"),
+				http.StatusBadRequest,
+			))
+
+			return
+		}
+	}
+
 	if request.TemplateVersion == "latest" {
 		request.TemplateVersion = ""
 	}

+ 116 - 0
api/server/handlers/v1/template/get.go

@@ -0,0 +1,116 @@
+package template
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"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/helm/loader"
+	"github.com/porter-dev/porter/internal/helm/repo"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/templater/parser"
+)
+
+type TemplateGetHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewTemplateGetHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *TemplateGetHandler {
+	return &TemplateGetHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *TemplateGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.GetTemplateRequest{}
+
+	ok := t.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	if request.RepoURL == "" {
+		request.RepoURL = t.Config().ServerConf.DefaultApplicationHelmRepoURL
+	}
+
+	hrs, err := t.Repo().HelmRepo().ListHelmReposByProjectID(project.ID)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	isValid := repo.ValidateRepoURL(t.Config().ServerConf.DefaultAddonHelmRepoURL, t.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, request.RepoURL)
+
+	if !isValid {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("invalid repo_url parameter"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	name, _ := requestutils.GetURLParamString(r, types.URLParamTemplateName)
+
+	if name == "" {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("template name is required"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	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 := loader.LoadChartPublic(request.RepoURL, name, version)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	parserDef := &parser.ClientConfigDefault{
+		HelmChart: chart,
+	}
+
+	res := &types.GetTemplateResponse{}
+	res.Metadata = chart.Metadata
+	res.Values = chart.Values
+	res.RepoURL = request.RepoURL
+
+	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)
+}

+ 115 - 0
api/server/handlers/v1/template/get_upgrade_notes.go

@@ -0,0 +1,115 @@
+package template
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"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/helm/loader"
+	"github.com/porter-dev/porter/internal/helm/repo"
+	"github.com/porter-dev/porter/internal/helm/upgrade"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type TemplateGetUpgradeNotesHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewTemplateGetUpgradeNotesHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *TemplateGetUpgradeNotesHandler {
+	return &TemplateGetUpgradeNotesHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *TemplateGetUpgradeNotesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.GetTemplateUpgradeNotesRequest{}
+
+	ok := t.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	hrs, err := t.Repo().HelmRepo().ListHelmReposByProjectID(project.ID)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	isValid := repo.ValidateRepoURL(t.Config().ServerConf.DefaultAddonHelmRepoURL, t.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, request.RepoURL)
+
+	if !isValid {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("invalid repo_url parameter"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	name, _ := requestutils.GetURLParamString(r, types.URLParamTemplateName)
+
+	if name == "" {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("template name is required"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	version, _ := requestutils.GetURLParamString(r, types.URLParamTemplateVersion)
+
+	// if version passed as latest, pass empty string to loader to get latest
+	if version == "latest" {
+		version = ""
+	}
+
+	prevVersion := request.PrevVersion
+
+	if prevVersion == "" {
+		prevVersion = "v0.0.0"
+	}
+
+	chart, err := loader.LoadChartPublic(request.RepoURL, name, version)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := &upgrade.UpgradeFile{}
+
+	for _, file := range chart.Files {
+		if strings.Contains(file.Name, "upgrade.yaml") {
+			upgradeFile, err := upgrade.ParseUpgradeFileFromBytes(file.Data)
+
+			if err != nil {
+				break
+			}
+
+			upgradeFile, err = upgradeFile.GetUpgradeFileBetweenVersions(prevVersion, version)
+
+			if err != nil {
+				break
+			}
+
+			res = upgradeFile
+		}
+	}
+
+	t.WriteResult(w, r, res)
+}

+ 76 - 0
api/server/handlers/v1/template/list.go

@@ -0,0 +1,76 @@
+package template
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"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/types"
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/helm/repo"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type TemplateListHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewTemplateListHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *TemplateListHandler {
+	return &TemplateListHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (t *TemplateListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	request := &types.ListTemplatesRequest{}
+
+	ok := t.DecodeAndValidate(w, r, request)
+
+	if !ok {
+		return
+	}
+
+	repoURL := request.RepoURL
+
+	if repoURL == "" {
+		repoURL = t.Config().ServerConf.DefaultApplicationHelmRepoURL
+	}
+
+	hrs, err := t.Repo().HelmRepo().ListHelmReposByProjectID(project.ID)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	isValid := repo.ValidateRepoURL(t.Config().ServerConf.DefaultAddonHelmRepoURL, t.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, repoURL)
+
+	if !isValid {
+		t.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("invalid repo_url parameter"),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	repoIndex, err := loader.LoadRepoIndexPublic(repoURL)
+
+	if err != nil {
+		t.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	porterCharts := loader.RepoIndexToPorterChartList(repoIndex, repoURL)
+
+	t.WriteResult(w, r, porterCharts)
+}

+ 206 - 1
api/server/router/v1/project.go

@@ -1,14 +1,18 @@
 package v1
 
 import (
+	"fmt"
+
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/router"
 	"github.com/porter-dev/porter/api/types"
+
+	v1Template "github.com/porter-dev/porter/api/server/handlers/v1/template"
 )
 
-// swagger:parameters createRegistry listRegistries
+// swagger:parameters createRegistry listRegistries listTemplates
 type projectPathParams struct {
 	// The project id
 	// in: path
@@ -17,6 +21,27 @@ type projectPathParams struct {
 	ProjectID uint `json:"project_id"`
 }
 
+// swagger:parameters getTemplate getTemplateUpgradeNotes
+type getTemplatePathParams struct {
+	// The project id
+	// in: path
+	// required: true
+	// minimum: 1
+	ProjectID uint `json:"project_id"`
+
+	// The name of the template
+	// in: path
+	// required: true
+	// type: string
+	Name string `json:"name"`
+
+	// The version of the template
+	// in: path
+	// required: true
+	// type: string
+	Version string `json:"version"`
+}
+
 func NewV1ProjectScopedRegisterer(children ...*router.Registerer) *router.Registerer {
 	return &router.Registerer{
 		GetRoutes: GetV1ProjectScopedRoutes,
@@ -61,5 +86,185 @@ func getV1ProjectRoutes(
 
 	var routes []*router.Route
 
+	// GET /api/v1/projects/{project_id}/templates -> v1Template.NewTemplateListHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/templates listTemplates
+	//
+	// Lists templates for a given `repo_url`.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: List templates
+	// tags:
+	// - Templates
+	// parameters:
+	//   - name: project_id
+	//   - name: repo_url
+	//     in: query
+	//     description: |
+	//       The full path (including scheme) of the Helm registry to list templates from.
+	//     type: string
+	// responses:
+	//   '200':
+	//     description: Successfully listed templates
+	//     schema:
+	//       $ref: '#/definitions/ListTemplatesResponse'
+	//   '400':
+	//     description: A malformed or bad request
+	//   '403':
+	//     description: Forbidden
+	listTemplatesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbList,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/templates", relPath),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listTemplatesRequest := v1Template.NewTemplateListHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listTemplatesEndpoint,
+		Handler:  listTemplatesRequest,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/templates/{name}/versions/{version} -> v1Template.NewTemplateGetHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/templates/{name}/versions/{version} getTemplate
+	//
+	// Retrieves a given template by a `name` and a `version`
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Get template
+	// tags:
+	// - Templates
+	// parameters:
+	//   - name: project_id
+	//   - name: name
+	//   - name: version
+	//   - name: repo_url
+	//     in: query
+	//     description: |
+	//       The full path (including scheme) of the Helm registry to list templates from.
+	//     type: string
+	// responses:
+	//   '200':
+	//     description: Successfully got the template
+	//     schema:
+	//       $ref: '#/definitions/GetTemplateResponse'
+	//   '400':
+	//     description: A malformed or bad request
+	//   '403':
+	//     description: Forbidden
+	getTemplateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/templates/{%s}/versions/{%s}",
+					relPath,
+					types.URLParamTemplateName,
+					types.URLParamTemplateVersion,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getTemplateRequest := v1Template.NewTemplateGetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getTemplateEndpoint,
+		Handler:  getTemplateRequest,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/templates/{name}/versions/{version}/upgrade_notes -> v1Template.NewTemplateGetUpgradeNotesHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/templates/{name}/versions/{version}/upgrade_notes getTemplateUpgradeNotes
+	//
+	// Retrieves a given template by a `name` and a `version`
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Get template upgrade notes
+	// tags:
+	// - Templates
+	// parameters:
+	//   - name: project_id
+	//   - name: name
+	//   - name: version
+	//   - name: prev_version
+	//     in: query
+	//     description: |
+	//       The previous version of the templates to generate upgrade notes from.
+	//     type: string
+	//   - name: repo_url
+	//     in: query
+	//     description: |
+	//       The full path (including scheme) of the Helm registry to list templates from.
+	//     type: string
+	// responses:
+	//   '200':
+	//     description: Successfully got the upgrade notes
+	//     schema:
+	//       $ref: '#/definitions/GetTemplateUpgradeNotesResponse'
+	//   '400':
+	//     description: A malformed or bad request
+	//   '403':
+	//     description: Forbidden
+	getTemplateUpgradeNotesEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"/templates/{%s}/versions/{%s}/upgrade_notes",
+					types.URLParamTemplateName,
+					types.URLParamTemplateVersion,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getTemplateUpgradeNotesRequest := v1Template.NewTemplateGetUpgradeNotesHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getTemplateUpgradeNotesEndpoint,
+		Handler:  getTemplateUpgradeNotesRequest,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 17 - 5
api/types/template.go

@@ -19,14 +19,24 @@ type ListTemplatesRequest struct {
 }
 
 type PorterTemplateSimple struct {
-	Name        string   `json:"name"`
-	Versions    []string `json:"versions"`
-	Description string   `json:"description"`
-	Icon        string   `json:"icon"`
-	RepoURL     string   `json:"repo_url,omitempty"`
+	// The name of the template
+	Name string `json:"name"`
+
+	// The list of valid versions for the template
+	Versions []string `json:"versions"`
+
+	// A description for the template
+	Description string `json:"description"`
+
+	// An image URI for the icon
+	Icon string `json:"icon"`
+
+	// The repo URL for the template
+	RepoURL string `json:"repo_url,omitempty"`
 }
 
 // ListTemplatesResponse is how a chart gets displayed when listed
+// swagger:model ListTemplatesResponse
 type ListTemplatesResponse []PorterTemplateSimple
 
 type GetTemplateRequest struct {
@@ -34,6 +44,7 @@ type GetTemplateRequest struct {
 }
 
 // GetTemplateResponse is a chart with detailed information and a form for reading
+// swagger:model GetTemplateResponse
 type GetTemplateResponse struct {
 	Markdown string                 `json:"markdown"`
 	Metadata *chart.Metadata        `json:"metadata"`
@@ -47,4 +58,5 @@ type GetTemplateUpgradeNotesRequest struct {
 	PrevVersion string `schema:"prev_version"`
 }
 
+// swagger:model GetTemplateUpgradeNotesResponse
 type GetTemplateUpgradeNotesResponse upgrade.UpgradeFile

+ 8 - 8
cli/cmd/apply.go

@@ -239,21 +239,21 @@ func NewDeployDriver(resource *switchboardModels.Resource, opts *drivers.SharedD
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := preview.GetSource(resource.Name, resource.Source)
+	target, err := preview.GetTarget(resource.Name, resource.Target)
 
 	if err != nil {
 		return nil, err
 	}
 
-	driver.source = source
+	driver.target = target
 
-	target, err := preview.GetTarget(resource.Name, resource.Target)
+	source, err := preview.GetSource(target.Project, resource.Name, resource.Source)
 
 	if err != nil {
 		return nil, err
 	}
 
-	driver.target = target
+	driver.source = source
 
 	return driver, nil
 }
@@ -958,7 +958,7 @@ func (t *DeploymentHook) PostApply(populatedData map[string]interface{}) error {
 	}
 
 	for _, res := range t.resourceGroup.Resources {
-		releaseType := getReleaseType(res)
+		releaseType := getReleaseType(t.projectID, res)
 		releaseName := getReleaseName(res)
 
 		if releaseType != "" && releaseName != "" {
@@ -1027,7 +1027,7 @@ func (t *DeploymentHook) OnConsolidatedErrors(allErrors map[string]error) {
 			if _, ok := allErrors[res.Name]; !ok {
 				req.SuccessfulResources = append(req.SuccessfulResources, &types.SuccessfullyDeployedResource{
 					ReleaseName: getReleaseName(res),
-					ReleaseType: getReleaseType(res),
+					ReleaseType: getReleaseType(t.projectID, res),
 				})
 			}
 		}
@@ -1150,10 +1150,10 @@ func getReleaseName(res *switchboardTypes.Resource) string {
 	return res.Name
 }
 
-func getReleaseType(res *switchboardTypes.Resource) string {
+func getReleaseType(projectID uint, res *switchboardTypes.Resource) string {
 	// can ignore the error because this method is called once
 	// GetSource has alrealy been called and validated previously
-	source, _ := preview.GetSource(res.Name, res.Source)
+	source, _ := preview.GetSource(projectID, res.Name, res.Source)
 
 	if source != nil && source.Name != "" {
 		return source.Name

+ 4 - 2
cli/cmd/deploy/create.go

@@ -451,6 +451,7 @@ func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, err
 func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, error) {
 	resp, err := c.Client.ListTemplates(
 		context.Background(),
+		c.CreateOpts.ProjectID,
 		&types.ListTemplatesRequest{},
 	)
 
@@ -478,9 +479,10 @@ func (c *CreateAgent) GetLatestTemplateVersion(templateName string) (string, err
 
 // GetLatestTemplateDefaultValues gets the default config (`values.yaml`) set for a specific
 // template.
-func (c *CreateAgent) GetLatestTemplateDefaultValues(templateName, templateVersion string) (map[string]interface{}, error) {
+func (c *CreateAgent) GetLatestTemplateDefaultValues(projectID uint, templateName, templateVersion string) (map[string]interface{}, error) {
 	chart, err := c.Client.GetTemplate(
 		context.Background(),
+		projectID,
 		templateName,
 		templateVersion,
 		&types.GetTemplateRequest{},
@@ -502,7 +504,7 @@ func (c *CreateAgent) GetMergedValues(overrideValues map[string]interface{}) (st
 	}
 
 	// get the values of the template
-	values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.Kind, latestVersion)
+	values, err := c.GetLatestTemplateDefaultValues(c.CreateOpts.ProjectID, c.CreateOpts.Kind, latestVersion)
 
 	if err != nil {
 		return "", nil, err

+ 6 - 4
cli/cmd/preview/build_image_driver.go

@@ -33,19 +33,21 @@ func NewBuildDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := GetSource(resource.Name, resource.Source)
+	target, err := GetTarget(resource.Name, resource.Target)
+
 	if err != nil {
 		return nil, err
 	}
 
-	driver.source = source
+	driver.target = target
+
+	source, err := GetSource(target.Project, resource.Name, resource.Source)
 
-	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 		return nil, err
 	}
 
-	driver.target = target
+	driver.source = source
 
 	return driver, nil
 }

+ 6 - 4
cli/cmd/preview/update_config_driver.go

@@ -34,19 +34,21 @@ func NewUpdateConfigDriver(resource *models.Resource, opts *drivers.SharedDriver
 		output:      make(map[string]interface{}),
 	}
 
-	source, err := GetSource(resource.Name, resource.Source)
+	target, err := GetTarget(resource.Name, resource.Target)
+
 	if err != nil {
 		return nil, err
 	}
 
-	driver.source = source
+	driver.target = target
+
+	source, err := GetSource(driver.target.Project, resource.Name, resource.Source)
 
-	target, err := GetTarget(resource.Name, resource.Target)
 	if err != nil {
 		return nil, err
 	}
 
-	driver.target = target
+	driver.source = source
 
 	return driver, nil
 }

+ 7 - 5
cli/cmd/preview/utils.go

@@ -11,7 +11,7 @@ import (
 	"github.com/porter-dev/porter/internal/integrations/preview"
 )
 
-func GetSource(resourceName string, input map[string]interface{}) (*preview.Source, error) {
+func GetSource(projectID uint, resourceName string, input map[string]interface{}) (*preview.Source, error) {
 	output := &preview.Source{}
 
 	// first read from env vars
@@ -64,7 +64,7 @@ func GetSource(resourceName string, input map[string]interface{}) (*preview.Sour
 	if output.Repo == "" {
 		output.Repo = "https://charts.getporter.dev"
 
-		values, err := existsInRepo(output.Name, output.Version, output.Repo)
+		values, err := existsInRepo(projectID, output.Name, output.Version, output.Repo)
 
 		if err == nil {
 			// found in "https://charts.getporter.dev"
@@ -75,7 +75,7 @@ func GetSource(resourceName string, input map[string]interface{}) (*preview.Sour
 
 		output.Repo = "https://chart-addons.getporter.dev"
 
-		values, err = existsInRepo(output.Name, output.Version, output.Repo)
+		values, err = existsInRepo(projectID, output.Name, output.Version, output.Repo)
 
 		if err == nil {
 			// found in https://chart-addons.getporter.dev
@@ -87,7 +87,7 @@ func GetSource(resourceName string, input map[string]interface{}) (*preview.Sour
 			"'https://charts.getporter.dev' or 'https://chart-addons.getporter.dev'", resourceName)
 	} else {
 		// we look in the passed-in repo
-		values, err := existsInRepo(output.Name, output.Version, output.Repo)
+		values, err := existsInRepo(projectID, output.Name, output.Version, output.Repo)
 
 		if err == nil {
 			output.SourceValues = values
@@ -175,9 +175,10 @@ func GetTarget(resourceName string, input map[string]interface{}) (*preview.Targ
 	return output, nil
 }
 
-func existsInRepo(name, version, url string) (map[string]interface{}, error) {
+func existsInRepo(projectID uint, name, version, url string) (map[string]interface{}, error) {
 	chart, err := config.GetAPIClient().GetTemplate(
 		context.Background(),
+		projectID,
 		name, version,
 		&types.GetTemplateRequest{
 			TemplateGetBaseRequest: types.TemplateGetBaseRequest{
@@ -185,6 +186,7 @@ func existsInRepo(name, version, url string) (map[string]interface{}, error) {
 			},
 		},
 	)
+
 	if err != nil {
 		return nil, err
 	}

+ 4 - 2
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/NewAppResource/_TemplateSelector.tsx

@@ -11,7 +11,7 @@ import TitleSection from "components/TitleSection";
 import { Context } from "shared/Context";
 
 const TemplateSelector = () => {
-  const { capabilities } = useContext(Context);
+  const { capabilities, currentProject } = useContext(Context);
 
   const [templates, setTemplates] = useState<PorterTemplate[]>([]);
   const [selectedVersion, setSelectedVersion] = useState<{
@@ -28,7 +28,9 @@ const TemplateSelector = () => {
         {
           repo_url: capabilities?.default_app_helm_repo_url,
         },
-        {}
+        {
+          project_id: currentProject.id,
+        }
       );
       let sortedVersionData = res.data
         .map((template: PorterTemplate) => {

+ 6 - 2
dashboard/src/main/home/cluster-dashboard/stacks/components/NewAppResourceForm.tsx

@@ -64,7 +64,7 @@ const NewAppResourceForm = (props: {
     onSubmit,
   } = props;
 
-  const { currentCluster } = useContext(Context);
+  const { currentProject, currentCluster } = useContext(Context);
 
   const [hasError, setHasError] = useState(false);
   const [isLoading, setIsLoading] = useState(true);
@@ -209,7 +209,11 @@ const NewAppResourceForm = (props: {
       .getTemplateInfo<ExpandedPorterTemplate>(
         "<token>",
         {},
-        { name: templateInfo.name, version: templateInfo.version }
+        {
+          project_id: currentProject.id,
+          name: templateInfo.name,
+          version: templateInfo.version,
+        }
       )
       .then((res) => {
         if (isSubscribed) {

+ 4 - 2
dashboard/src/main/home/cluster-dashboard/stacks/launch/components/AddResourceButton.tsx

@@ -11,7 +11,7 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 
 export const AddResourceButton = () => {
-  const { capabilities } = useContext(Context);
+  const { currentProject, capabilities } = useContext(Context);
   const [templates, setTemplates] = useState<PorterTemplate[]>([]);
   const [currentTemplate, setCurrentTemplate] = useState<PorterTemplate>();
   const [currentVersion, setCurrentVersion] = useState("");
@@ -23,7 +23,9 @@ export const AddResourceButton = () => {
         {
           repo_url: capabilities?.default_app_helm_repo_url,
         },
-        {}
+        {
+          project_id: currentProject.id,
+        }
       );
       let sortedVersionData = res.data
         .map((template: PorterTemplate) => {

+ 31 - 4
dashboard/src/main/home/launch/Launch.tsx

@@ -45,6 +45,8 @@ type StateType = {
   tabOptions: TabOption[];
 };
 class Templates extends Component<PropsType, StateType> {
+  private previousContext: any;
+
   state = {
     currentTemplate: null as PorterTemplate | null,
     form: null as any,
@@ -58,7 +60,28 @@ class Templates extends Component<PropsType, StateType> {
     tabOptions: initialTabOptions,
   };
 
-  async componentDidMount() {
+  componentDidMount() {
+    this.previousContext = this.context;
+    this.setTemplatesAndRepos();
+  }
+
+  componentDidUpdate() {
+    // if project ID has changed, load in a new set of templates
+    if (
+      this.context.currentProject?.id != this.previousContext.currentProject?.id
+    ) {
+      this.setTemplatesAndRepos();
+    }
+
+    this.previousContext = this.context;
+  }
+
+  setTemplatesAndRepos = async () => {
+    // if the project ID is not defined, return
+    if (!this.context.currentProject) {
+      return;
+    }
+
     let default_addon_helm_repo_url = this.context?.capabilities
       ?.default_addon_helm_repo_url;
     let default_app_helm_repo_url = this.context?.capabilities
@@ -69,7 +92,9 @@ class Templates extends Component<PropsType, StateType> {
         {
           repo_url: default_addon_helm_repo_url,
         },
-        {}
+        {
+          project_id: this.context.currentProject.id,
+        }
       );
       let sortedVersionData = res.data.map((template: any) => {
         let versions = template.versions.reverse();
@@ -96,7 +121,9 @@ class Templates extends Component<PropsType, StateType> {
         {
           repo_url: default_app_helm_repo_url,
         },
-        {}
+        {
+          project_id: this.context.currentProject.id,
+        }
       );
       let sortedVersionData = res.data.map((template: any) => {
         let versions = template.versions.reverse();
@@ -191,7 +218,7 @@ class Templates extends Component<PropsType, StateType> {
     } catch (error) {
       this.setState({ loading: false, error: true });
     }
-  }
+  };
 
   isTryingToClone = () => {
     const queryParams = getQueryParams(this.props);

+ 1 - 0
dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx

@@ -82,6 +82,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
 
       api
         .getTemplateInfo("<token>", params, {
+          project_id: this.context.currentProject.id,
           name: this.props.currentTemplate.name.toLowerCase().trim(),
           version: this.props.currentTemplate.currentVersion,
         })

+ 1 - 0
dashboard/src/main/home/modals/UpgradeChartModal.tsx

@@ -45,6 +45,7 @@ export default class UpgradeChartModal extends Component<PropsType, StateType> {
           prev_version: this.props.currentChart.chart.metadata.version,
         },
         {
+          project_id: this.context.currentProject.id,
           name: chartName,
           version: this.props.currentChart.latest_version,
         }

+ 10 - 6
dashboard/src/shared/api.tsx

@@ -1228,9 +1228,9 @@ const getTemplateInfo = baseApi<
   {
     repo_url?: string;
   },
-  { name: string; version: string }
+  { project_id: number; name: string; version: string }
 >("GET", (pathParams) => {
-  return `/api/templates/${pathParams.name}/${pathParams.version}`;
+  return `/api/v1/projects/${pathParams.project_id}/templates/${pathParams.name}/versions/${pathParams.version}`;
 });
 
 const getTemplateUpgradeNotes = baseApi<
@@ -1238,17 +1238,21 @@ const getTemplateUpgradeNotes = baseApi<
     repo_url?: string;
     prev_version: string;
   },
-  { name: string; version: string }
+  { project_id: number; name: string; version: string }
 >("GET", (pathParams) => {
-  return `/api/templates/${pathParams.name}/${pathParams.version}/upgrade_notes`;
+  return `/api/v1/projects/${pathParams.project_id}/templates/${pathParams.name}/versions/${pathParams.version}/upgrade_notes`;
 });
 
 const getTemplates = baseApi<
   {
     repo_url?: string;
   },
-  {}
->("GET", "/api/templates");
+  {
+    project_id: number;
+  }
+>("GET", (pathParams) => {
+  return `/api/v1/projects/${pathParams.project_id}/templates`;
+});
 
 const getHelmRepos = baseApi<
   {},

+ 19 - 0
internal/helm/repo/repo.go

@@ -83,3 +83,22 @@ func (hr *HelmRepo) getChartBasic(
 
 	return loader.LoadChart(client, hr.RepoURL, chartName, chartVersion)
 }
+
+func ValidateRepoURL(
+	defaultAddonRepoURL, defaultAppRepoURL string,
+	hrs []*models.HelmRepo,
+	repo_url string,
+) bool {
+	if repo_url == defaultAddonRepoURL || repo_url == defaultAppRepoURL {
+		return true
+	}
+
+	// otherwise, iterate through helm repos
+	for _, hr := range hrs {
+		if hr.RepoURL == repo_url {
+			return true
+		}
+	}
+
+	return false
+}