Alexander Belanger 5 лет назад
Родитель
Сommit
59dd51442d
47 измененных файлов с 2312 добавлено и 216 удалено
  1. 123 0
      cli/cmd/api/helm_repo.go
  2. 46 0
      cli/cmd/api/integration.go
  3. 37 0
      cli/cmd/config.go
  4. 27 0
      cli/cmd/connect.go
  5. 97 0
      cli/cmd/connect/helm.go
  6. 127 0
      cli/cmd/helm_repo.go
  7. 0 123
      cli/cmd/test.go
  8. 4 0
      cmd/app/main.go
  9. 4 0
      cmd/migrate/main.go
  10. 29 0
      internal/forms/helm_repo.go
  11. 19 0
      internal/forms/integration.go
  12. 72 14
      internal/helm/loader/loader.go
  13. 82 0
      internal/helm/repo/repo.go
  14. 6 4
      internal/kubernetes/config.go
  15. 1 1
      internal/models/cluster.go
  16. 70 0
      internal/models/helm_repo.go
  17. 56 0
      internal/models/integrations/basic.go
  18. 31 9
      internal/models/integrations/integration.go
  19. 1 1
      internal/models/integrations/oauth.go
  20. 21 29
      internal/models/integrations/token_cache.go
  21. 4 0
      internal/models/project.go
  22. 6 5
      internal/registry/registry.go
  23. 1 1
      internal/repository/cluster.go
  24. 140 0
      internal/repository/gorm/auth.go
  25. 86 0
      internal/repository/gorm/auth_test.go
  26. 1 1
      internal/repository/gorm/cluster.go
  27. 5 3
      internal/repository/gorm/cluster_test.go
  28. 196 0
      internal/repository/gorm/helm_repo.go
  29. 250 0
      internal/repository/gorm/helm_repo_test.go
  30. 55 1
      internal/repository/gorm/helpers_test.go
  31. 4 2
      internal/repository/gorm/registry_test.go
  32. 2 0
      internal/repository/gorm/repository.go
  33. 16 0
      internal/repository/helm_repo.go
  34. 8 0
      internal/repository/integrations.go
  35. 2 0
      internal/repository/repository.go
  36. 64 0
      internal/repository/test/auth.go
  37. 1 1
      internal/repository/test/cluster.go
  38. 125 0
      internal/repository/test/helm_repo.go
  39. 2 0
      internal/repository/test/repository.go
  40. 1 1
      server/api/cluster_handler_test.go
  41. 1 1
      server/api/deploy_handler.go
  42. 131 0
      server/api/helm_repo_handler.go
  43. 157 0
      server/api/helm_repo_handler_test.go
  44. 77 1
      server/api/integration_handler.go
  45. 71 1
      server/api/integration_handler_test.go
  46. 4 17
      server/api/template_handler.go
  47. 49 0
      server/router/router.go

+ 123 - 0
cli/cmd/api/helm_repo.go

@@ -0,0 +1,123 @@
+package api
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// CreateHelmRepoRequest represents the accepted fields for creating
+// a Helm repository with basic authentication
+type CreateHelmRepoRequest struct {
+	Name               string `json:"name"`
+	RepoURL            string `json:"repo_url"`
+	BasicIntegrationID uint   `json:"basic_integration_id"`
+}
+
+// CreateHelmRepoResponse is the resulting helm repo after creation
+type CreateHelmRepoResponse models.HelmRepoExternal
+
+// CreateHelmRepo creates an Helm repository integration with basic authentication
+func (c *Client) CreateHelmRepo(
+	ctx context.Context,
+	projectID uint,
+	createHR *CreateHelmRepoRequest,
+) (*CreateHelmRepoResponse, error) {
+	data, err := json.Marshal(createHR)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(
+		"POST",
+		fmt.Sprintf("%s/projects/%d/helmrepos", c.BaseURL, projectID),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &CreateHelmRepoResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}
+
+// ListHelmRepoResponse is the list of Helm repos for a project
+type ListHelmRepoResponse []models.HelmRepoExternal
+
+// ListHelmRepos returns a list of Helm repos for a project
+func (c *Client) ListHelmRepos(
+	ctx context.Context,
+	projectID uint,
+) (ListHelmRepoResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/helmrepos", c.BaseURL, projectID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &ListHelmRepoResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return *bodyResp, nil
+}
+
+// ListChartsResponse is the list of charts in a Helm repository
+type ListChartsResponse []models.PorterChartList
+
+// ListCharts lists the charts in a Helm repository
+func (c *Client) ListCharts(
+	ctx context.Context,
+	projectID uint,
+	helmID uint,
+) (ListChartsResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/helmrepos/%d/charts", c.BaseURL, projectID, helmID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &ListChartsResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return *bodyResp, nil
+}

+ 46 - 0
cli/cmd/api/integration.go

@@ -101,3 +101,49 @@ func (c *Client) CreateGCPIntegration(
 
 	return bodyResp, nil
 }
+
+// CreateBasicAuthIntegrationRequest represents the accepted fields for creating
+// a "basic auth" integration
+type CreateBasicAuthIntegrationRequest struct {
+	Username string `json:"username"`
+	Password string `json:"password"`
+}
+
+// CreateBasicAuthIntegrationResponse is the resulting integration after creation
+type CreateBasicAuthIntegrationResponse ints.BasicIntegrationExternal
+
+// CreateBasicAuthIntegration creates a "basic auth" integration
+func (c *Client) CreateBasicAuthIntegration(
+	ctx context.Context,
+	projectID uint,
+	createBasic *CreateBasicAuthIntegrationRequest,
+) (*CreateBasicAuthIntegrationResponse, error) {
+	data, err := json.Marshal(createBasic)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(
+		"POST",
+		fmt.Sprintf("%s/projects/%d/integrations/basic", c.BaseURL, projectID),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &CreateBasicAuthIntegrationResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}

+ 37 - 0
cli/cmd/config.go

@@ -19,6 +19,7 @@ var (
 	projectID  uint
 	registryID uint
 	clusterID  uint
+	helmRepoID uint
 )
 
 var configCmd = &cobra.Command{
@@ -95,6 +96,27 @@ var setRegistryCmd = &cobra.Command{
 	},
 }
 
+var setHelmRepoCmd = &cobra.Command{
+	Use:   "set-helmrepo [id]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Saves the helm repo id in the default configuration",
+	Run: func(cmd *cobra.Command, args []string) {
+		hrID, err := strconv.ParseUint(args[0], 10, 64)
+
+		if err != nil {
+			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			os.Exit(1)
+		}
+
+		err = setHelmRepo(uint(hrID))
+
+		if err != nil {
+			color.New(color.FgRed).Printf("An error occurred: %v\n", err)
+			os.Exit(1)
+		}
+	},
+}
+
 var setHostCmd = &cobra.Command{
 	Use:   "set-host [host]",
 	Args:  cobra.ExactArgs(1),
@@ -116,6 +138,7 @@ func init() {
 	configCmd.AddCommand(setClusterCmd)
 	configCmd.AddCommand(setHostCmd)
 	configCmd.AddCommand(setRegistryCmd)
+	configCmd.AddCommand(setHelmRepoCmd)
 }
 
 func setDriver(driver string) error {
@@ -167,6 +190,12 @@ func setRegistry(id uint) error {
 	return viper.WriteConfig()
 }
 
+func setHelmRepo(id uint) error {
+	viper.Set("helm_repo", id)
+	color.New(color.FgGreen).Printf("Set the current helm repo id as %d\n", id)
+	return viper.WriteConfig()
+}
+
 func setHost(host string) error {
 	viper.Set("host", host)
 	err := viper.WriteConfig()
@@ -198,6 +227,14 @@ func getRegistryID() uint {
 	return viper.GetUint("registry")
 }
 
+func getHelmRepoID() uint {
+	if helmRepoID != 0 {
+		return helmRepoID
+	}
+
+	return viper.GetUint("helm_repo")
+}
+
 func getProjectID() uint {
 	if projectID != 0 {
 		return projectID

+ 27 - 0
cli/cmd/connect.go

@@ -55,6 +55,19 @@ var connectGCRCmd = &cobra.Command{
 	},
 }
 
+var connectHRCmd = &cobra.Command{
+	Use:     "helmrepo",
+	Aliases: []string{"helm", "helmrepos"},
+	Short:   "Adds a Helm repository to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, runConnectHelmRepoBasic)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 func init() {
 	rootCmd.AddCommand(connectCmd)
 
@@ -90,6 +103,7 @@ func init() {
 
 	connectCmd.AddCommand(connectECRCmd)
 	connectCmd.AddCommand(connectGCRCmd)
+	connectCmd.AddCommand(connectHRCmd)
 }
 
 func runConnectKubeconfig(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
@@ -139,3 +153,16 @@ func runConnectGCR(_ *api.AuthCheckResponse, client *api.Client, _ []string) err
 
 	return setRegistry(regID)
 }
+
+func runConnectHelmRepoBasic(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
+	hrID, err := connect.Helm(
+		client,
+		getProjectID(),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return setHelmRepo(hrID)
+}

+ 97 - 0
cli/cmd/connect/helm.go

@@ -0,0 +1,97 @@
+package connect
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+)
+
+// Helm connects a Helm repository using HTTP basic authentication
+func Helm(
+	client *api.Client,
+	projectID uint,
+) (uint, error) {
+	// if project ID is 0, ask the user to set the project ID or create a project
+	if projectID == 0 {
+		return 0, fmt.Errorf("no project set, please run porter project set [id]")
+	}
+
+	// query for helm repo name
+	helmName, err := utils.PromptPlaintext(fmt.Sprintf(`Give this Helm repository a name: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	repoURL, err := utils.PromptPlaintext(fmt.Sprintf(`Provide the Helm repository URL: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(`Does this endpoint require a username/password to authenticate? %s `,
+			color.New(color.FgCyan).Sprintf("[y/n]"),
+		),
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	username := ""
+	password := ""
+
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		username, err = utils.PromptPlaintext(fmt.Sprintf(`Username: `))
+
+		if err != nil {
+			return 0, err
+		}
+
+		password, err = utils.PromptPasswordWithConfirmation()
+
+		if err != nil {
+			return 0, err
+		}
+	}
+
+	// create the basic auth integration
+	integration, err := client.CreateBasicAuthIntegration(
+		context.Background(),
+		projectID,
+		&api.CreateBasicAuthIntegrationRequest{
+			Username: username,
+			Password: password,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
+
+	// create the helm repo
+	hr, err := client.CreateHelmRepo(
+		context.Background(),
+		projectID,
+		&api.CreateHelmRepoRequest{
+			Name:               helmName,
+			RepoURL:            repoURL,
+			BasicIntegrationID: integration.ID,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created helm repo with id %d and name %s\n", hr.ID, hr.Name)
+
+	return hr.ID, nil
+}

+ 127 - 0
cli/cmd/helm_repo.go

@@ -0,0 +1,127 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/spf13/cobra"
+)
+
+// helmRepoCmd represents the "porter helmrepo" base command when called
+// without any subcommands
+var helmRepoCmd = &cobra.Command{
+	Use:     "helmrepo",
+	Aliases: []string{"helm", "helmrepos"},
+	Short:   "Commands that read from a connected Helm repository",
+}
+
+var helmRepoListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists the Helm repositories linked to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, listHelmRepos)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var helmRepoChartCmd = &cobra.Command{
+	Use:     "chart",
+	Aliases: []string{"charts"},
+	Short:   "Commands for interacting with Helm repository charts",
+}
+
+var helmRepoChartListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "Lists charts in the default Helm repository",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, listHelmRepoCharts)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(helmRepoCmd)
+
+	helmRepoCmd.PersistentFlags().UintVar(
+		&helmRepoID,
+		"helmrepo-id",
+		0,
+		"id of the helm repo",
+	)
+
+	helmRepoCmd.AddCommand(helmRepoListCmd)
+	helmRepoCmd.AddCommand(helmRepoChartCmd)
+
+	helmRepoChartCmd.AddCommand(helmRepoChartListCmd)
+}
+
+func listHelmRepos(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	pID := getProjectID()
+
+	hrs, err := client.ListHelmRepos(
+		context.Background(),
+		pID,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
+
+	fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "ID", "NAME", "URL", "SERVICE")
+
+	currHelmID := getHelmRepoID()
+
+	for _, hr := range hrs {
+		if currHelmID == hr.ID {
+			color.New(color.FgGreen).Fprintf(w, "%d\t%s\t%s\t%s (current helm repo)\n", hr.ID, hr.Name, hr.RepoURL, hr.Service)
+		} else {
+			fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", hr.ID, hr.Name, hr.RepoURL, hr.Service)
+		}
+	}
+
+	w.Flush()
+
+	return nil
+}
+
+func listHelmRepoCharts(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	pID := getProjectID()
+	hrID := getHelmRepoID()
+
+	charts, err := client.ListCharts(
+		context.Background(),
+		pID,
+		hrID,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
+
+	fmt.Fprintf(w, "%s\t%s\n", "NAME", "VERSION")
+
+	for _, chart := range charts {
+		fmt.Fprintf(w, "%s\t%s\n", strings.ToLower(chart.Name), chart.Version)
+	}
+
+	w.Flush()
+
+	return nil
+}

+ 0 - 123
cli/cmd/test.go

@@ -1,123 +0,0 @@
-package cmd
-
-import (
-	"encoding/json"
-	"fmt"
-	"os"
-
-	"github.com/fatih/color"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/spf13/cobra"
-)
-
-var testCmd = &cobra.Command{
-	Use:   "test",
-	Short: "Testing",
-	Run: func(cmd *cobra.Command, args []string) {
-		// chart, err := loader.LoadChart("https://porter-dev.github.io/chart-repo", "docker", "0.0.1")
-
-		// if err != nil {
-		// 	red := color.New(color.FgRed)
-		// 	red.Println("Error running test:", err.Error())
-		// 	os.Exit(1)
-		// }
-
-		// bytes, err := yaml.Marshal(chart)
-
-		// if err != nil {
-		// 	red := color.New(color.FgRed)
-		// 	red.Println("Error running test:", err.Error())
-		// 	os.Exit(1)
-		// }
-
-		// fmt.Println(string(bytes))
-
-		form := &models.FormYAML{
-			Tabs: []*models.FormTab{
-				// &models.FormTab{
-				// 	Context: &models.FormContext{
-				// 		Type: "helm/values",
-				// 	},
-				// 	Name:  "main",
-				// 	Label: "Main Settings",
-				// 	Sections: []*models.FormSection{
-				// 		&models.FormSection{
-				// 			Name: "section_one",
-				// 			Contents: []*models.FormContent{
-				// 				&models.FormContent{
-				// 					Type:  "number-input",
-				// 					Value: "service.targetPort",
-				// 					Label: "Target Port",
-				// 					Settings: struct {
-				// 						Default interface{} `yaml:"default,omitempty" json:"default,omitempty"`
-				// 						Unit    interface{} `yaml:"unit,omitempty" json:"unit,omitempty"`
-				// 					}{
-				// 						Default: 8000,
-				// 					},
-				// 				},
-				// 			},
-				// 		},
-				// 	},
-				// },
-				&models.FormTab{
-					Context: &models.FormContext{
-						Type: "cluster",
-						Config: map[string]string{
-							"group":    "apps",
-							"version":  "v1",
-							"resource": "deployments",
-						},
-					},
-					Name:  "crd",
-					Label: "CRDs",
-					Sections: []*models.FormSection{
-						&models.FormSection{
-							Name: "section_one",
-							Contents: []*models.FormContent{
-								&models.FormContent{
-									Type:  "resourcelist",
-									Value: `[{"name": "certificate_1","namespace": "default","status": "Ready" },{"name": "certificate_2","namespace": "default","status": "Issuing" }]`,
-								},
-							},
-						},
-					},
-				},
-			},
-		}
-
-		bytes, err := json.Marshal(form)
-
-		if err != nil {
-			red := color.New(color.FgRed)
-			red.Println("Error running test:", err.Error())
-			os.Exit(1)
-		}
-
-		fmt.Println(string(bytes))
-	},
-}
-
-func init() {
-	rootCmd.AddCommand(testCmd)
-}
-
-// // FormSection is a section of a form
-// type FormSection struct {
-// 	Context  *FormContext   `yaml:"context" json:"context"`
-// 	Name     string         `yaml:"name" json:"name"`
-// 	ShowIf   string         `yaml:"show_if" json:"show_if"`
-// 	Contents []*FormContent `yaml:"contents" json:"contents,omitempty"`
-// }
-
-// // FormContent is a form's atomic unit
-// type FormContent struct {
-// 	Context  *FormContext `yaml:"context" json:"context"`
-// 	Type     string       `yaml:"type" json:"type"`
-// 	Label    string       `yaml:"label" json:"label"`
-// 	Name     string       `yaml:"name,omitempty" json:"name,omitempty"`
-// 	Value    interface{}  `yaml:"value,omitempty" json:"value,omitempty"`
-// 	Settings struct {
-// 		Default interface{} `yaml:"default,omitempty" json:"default,omitempty"`
-// 		Unit    interface{} `yaml:"unit,omitempty" json:"unit,omitempty"`
-// 	} `yaml:"settings,omitempty" json:"settings,omitempty"`
-// }

+ 4 - 0
cmd/app/main.go

@@ -40,16 +40,20 @@ func main() {
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},
+		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},
 		&models.ClusterResolver{},
 		&ints.KubeIntegration{},
+		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},
 		&ints.OAuthIntegration{},
 		&ints.GCPIntegration{},
 		&ints.AWSIntegration{},
 		&ints.TokenCache{},
+		&ints.ClusterTokenCache{},
 		&ints.RegTokenCache{},
+		&ints.HelmRepoTokenCache{},
 	)
 
 	if err != nil {

+ 4 - 0
cmd/migrate/main.go

@@ -31,16 +31,20 @@ func main() {
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},
+		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},
 		&models.ClusterResolver{},
 		&ints.KubeIntegration{},
+		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},
 		&ints.OAuthIntegration{},
 		&ints.GCPIntegration{},
 		&ints.AWSIntegration{},
 		&ints.TokenCache{},
+		&ints.ClusterTokenCache{},
 		&ints.RegTokenCache{},
+		&ints.HelmRepoTokenCache{},
 	)
 
 	if err != nil {

+ 29 - 0
internal/forms/helm_repo.go

@@ -0,0 +1,29 @@
+package forms
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// CreateHelmRepo represents the accepted values for creating a
+// helm repo
+type CreateHelmRepo struct {
+	Name      string `json:"name" form:"required"`
+	RepoURL   string `json:"repo_url" form:"required"`
+	ProjectID uint   `json:"project_id" form:"required"`
+
+	BasicIntegrationID uint `json:"basic_integration_id"`
+	GCPIntegrationID   uint `json:"gcp_integration_id"`
+	AWSIntegrationID   uint `json:"aws_integration_id"`
+}
+
+// ToHelmRepo converts the form to a gorm helm repo model
+func (ch *CreateHelmRepo) ToHelmRepo() (*models.HelmRepo, error) {
+	return &models.HelmRepo{
+		Name:                   ch.Name,
+		RepoURL:                ch.RepoURL,
+		ProjectID:              ch.ProjectID,
+		BasicAuthIntegrationID: ch.BasicIntegrationID,
+		GCPIntegrationID:       ch.GCPIntegrationID,
+		AWSIntegrationID:       ch.AWSIntegrationID,
+	}, nil
+}

+ 19 - 0
internal/forms/integration.go

@@ -21,6 +21,25 @@ func (cgf *CreateGCPIntegrationForm) ToGCPIntegration() (*ints.GCPIntegration, e
 	}, nil
 }
 
+// CreateBasicAuthIntegrationForm represents the accepted values for creating a
+// basic auth integration
+type CreateBasicAuthIntegrationForm struct {
+	UserID    uint   `json:"user_id" form:"required"`
+	ProjectID uint   `json:"project_id" form:"required"`
+	Username  string `json:"username"`
+	Password  string `json:"password"`
+}
+
+// ToBasicIntegration converts the project to a gorm project model
+func (cbf *CreateBasicAuthIntegrationForm) ToBasicIntegration() (*ints.BasicIntegration, error) {
+	return &ints.BasicIntegration{
+		UserID:    cbf.UserID,
+		ProjectID: cbf.ProjectID,
+		Username:  []byte(cbf.Username),
+		Password:  []byte(cbf.Password),
+	}, nil
+}
+
 // CreateAWSIntegrationForm represents the accepted values for creating an
 // AWS Integration
 type CreateAWSIntegrationForm struct {

+ 72 - 14
internal/helm/loader/loader.go

@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/models"
 	"k8s.io/helm/pkg/repo"
 	"sigs.k8s.io/yaml"
 
@@ -14,9 +15,48 @@ import (
 	chartloader "helm.sh/helm/v3/pkg/chart/loader"
 )
 
-// LoadRepoIndex loads an index file from a remote Helm repo
-func LoadRepoIndex(indexURL string) (*repo.IndexFile, error) {
-	resp, err := http.Get(indexURL)
+// RepoIndexToPorterChartList converts an index file to a list of porter charts
+func RepoIndexToPorterChartList(index *repo.IndexFile) []*models.PorterChartList {
+	porterCharts := make([]*models.PorterChartList, 0)
+
+	for _, entry := range index.Entries {
+		indexChart := entry[0]
+
+		porterChart := &models.PorterChartList{
+			Name:        indexChart.Name,
+			Version:     indexChart.Version,
+			Description: indexChart.Description,
+			Icon:        indexChart.Icon,
+		}
+
+		porterCharts = append(porterCharts, porterChart)
+	}
+
+	return porterCharts
+}
+
+// BasicAuthClient is just a username/password to set on requests
+type BasicAuthClient struct {
+	Username string
+	Password string
+}
+
+// LoadRepoIndex uses an http request to get the index file and loads it
+func LoadRepoIndex(client *BasicAuthClient, repoURL string) (*repo.IndexFile, error) {
+	trimmedRepoURL := strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
+	indexURL := trimmedRepoURL + "/index.yaml"
+
+	req, err := http.NewRequest("GET", indexURL, nil)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if client.Username != "" {
+		req.SetBasicAuth(client.Username, client.Password)
+	}
+
+	resp, err := http.DefaultClient.Do(req)
 
 	if err != nil {
 		return nil, err
@@ -43,14 +83,14 @@ func LoadRepoIndex(indexURL string) (*repo.IndexFile, error) {
 	return index, nil
 }
 
-// LoadChart returns a Helm3 (v2) chart from a remote repo. If chartVersion is an
-// empty string, the most stable latest version is found.
-//
-// TODO: this is an expensive operation, so after retrieving the digest from the
-// repo index, this should check the digest in the cache
-func LoadChart(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
-	trimmedRepoURL := strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
-	repoIndex, err := LoadRepoIndex(trimmedRepoURL + "/index.yaml")
+// LoadRepoIndexPublic loads an index file from a remote public Helm repo
+func LoadRepoIndexPublic(repoURL string) (*repo.IndexFile, error) {
+	return LoadRepoIndex(&BasicAuthClient{}, repoURL)
+}
+
+// LoadChart uses an http request to fetch a chart from a remote Helm repo
+func LoadChart(client *BasicAuthClient, repoURL, chartName, chartVersion string) (*chart.Chart, error) {
+	repoIndex, err := LoadRepoIndex(client, repoURL)
 
 	if err != nil {
 		return nil, err
@@ -64,12 +104,21 @@ func LoadChart(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
 		return nil, fmt.Errorf("%s:%s no valid download urls", chartName, chartVersion)
 	}
 
+	trimmedRepoURL := strings.TrimSuffix(strings.TrimSpace(repoURL), "/")
 	chartURL := trimmedRepoURL + "/" + strings.TrimPrefix(cv.URLs[0], "/")
 
-	fmt.Println(chartURL)
-
 	// download tgz
-	resp, err := http.Get(chartURL)
+	req, err := http.NewRequest("GET", chartURL, nil)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if client.Username != "" {
+		req.SetBasicAuth(client.Username, client.Password)
+	}
+
+	resp, err := http.DefaultClient.Do(req)
 
 	if err != nil {
 		return nil, err
@@ -85,3 +134,12 @@ func LoadChart(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
 
 	return chartloader.LoadArchive(bytes.NewReader(data))
 }
+
+// LoadChartPublic returns a Helm3 (v2) chart from a remote public repo.
+// If chartVersion is an empty string, the most stable latest version is found.
+//
+// TODO: this is an expensive operation, so after retrieving the digest from the
+// repo index, this should check the digest in the cache
+func LoadChartPublic(repoURL, chartName, chartVersion string) (*chart.Chart, error) {
+	return LoadChart(&BasicAuthClient{}, repoURL, chartName, chartVersion)
+}

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

@@ -0,0 +1,82 @@
+package repo
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/models"
+	"helm.sh/helm/v3/pkg/chart"
+
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+// HelmRepo wraps the gorm HelmRepo model
+type HelmRepo models.HelmRepo
+
+// ListCharts lists Porter charts for a given helm repo
+func (hr *HelmRepo) ListCharts(repo repository.Repository) ([]*models.PorterChartList, error) {
+	if hr.BasicAuthIntegrationID != 0 {
+		return hr.listChartsBasic(repo)
+	}
+
+	return nil, fmt.Errorf("error listing charts")
+}
+
+// GetChart retrieves a Porter chart for a given helm repo
+func (hr *HelmRepo) GetChart(
+	repo repository.Repository,
+	chartName, chartVersion string,
+) (*chart.Chart, error) {
+	if hr.BasicAuthIntegrationID != 0 {
+		return hr.getChartBasic(repo, chartName, chartVersion)
+	}
+
+	return nil, fmt.Errorf("error listing charts")
+}
+
+func (hr *HelmRepo) listChartsBasic(
+	repo repository.Repository,
+) ([]*models.PorterChartList, error) {
+	// get the basic auth integration
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		hr.BasicAuthIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := &loader.BasicAuthClient{
+		Username: string(basic.Username),
+		Password: string(basic.Password),
+	}
+
+	repoIndex, err := loader.LoadRepoIndex(client, hr.RepoURL)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return loader.RepoIndexToPorterChartList(repoIndex), nil
+}
+
+func (hr *HelmRepo) getChartBasic(
+	repo repository.Repository,
+	chartName, chartVersion string,
+) (*chart.Chart, error) {
+	// get the basic auth integration
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		hr.BasicAuthIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := &loader.BasicAuthClient{
+		Username: string(basic.Username),
+		Password: string(basic.Password),
+	}
+
+	return loader.LoadChart(client, hr.RepoURL, chartName, chartVersion)
+}

+ 6 - 4
internal/kubernetes/config.go

@@ -315,15 +315,17 @@ func (conf *OutOfClusterConfig) createRawConfigFromCluster() (*api.Config, error
 }
 
 func (conf *OutOfClusterConfig) getTokenCache() (tok *ints.TokenCache, err error) {
-	return &conf.Cluster.TokenCache, nil
+	return &conf.Cluster.TokenCache.TokenCache, nil
 }
 
 func (conf *OutOfClusterConfig) setTokenCache(token string, expiry time.Time) error {
 	_, err := conf.Repo.Cluster.UpdateClusterTokenCache(
-		&ints.TokenCache{
+		&ints.ClusterTokenCache{
 			ClusterID: conf.Cluster.ID,
-			Token:     []byte(token),
-			Expiry:    expiry,
+			TokenCache: ints.TokenCache{
+				Token:  []byte(token),
+				Expiry: expiry,
+			},
 		},
 	)
 

+ 1 - 1
internal/models/cluster.go

@@ -58,7 +58,7 @@ type Cluster struct {
 	AWSIntegrationID  uint
 
 	// A token cache that can be used by an auth mechanism, if desired
-	TokenCache integrations.TokenCache `json:"token_cache"`
+	TokenCache integrations.ClusterTokenCache `json:"token_cache"`
 
 	// CertificateAuthorityData for the cluster, encrypted at rest
 	CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`

+ 70 - 0
internal/models/helm_repo.go

@@ -0,0 +1,70 @@
+package models
+
+import (
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
+)
+
+// HelmRepo is an integration that can connect to a Helm repository via a
+// set of auth mechanisms
+type HelmRepo struct {
+	gorm.Model
+
+	// Name given to the Helm repository
+	Name string `json:"name"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// RepoURL is the URL to the helm repo. This varies based on the integration
+	// type. For example, for AWS S3 this may be prefixed with s3://, or for
+	// GCS it may be gs://
+	RepoURL string `json:"repo_url"`
+
+	// ------------------------------------------------------------------
+	// All fields below this line are encrypted before storage
+	// ------------------------------------------------------------------
+	BasicAuthIntegrationID uint
+	GCPIntegrationID       uint
+	AWSIntegrationID       uint
+
+	// A token cache that can be used by an auth mechanism (integration), if desired
+	TokenCache integrations.HelmRepoTokenCache
+}
+
+// HelmRepoExternal is an external HelmRepo to be shared over REST
+type HelmRepoExternal struct {
+	ID uint `json:"id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// Name of the repo
+	Name string `json:"name"`
+
+	RepoURL string `json:"repo_name"`
+
+	// The integration service for this registry
+	Service integrations.IntegrationService `json:"service"`
+}
+
+// Externalize generates an external Registry to be shared over REST
+func (hr *HelmRepo) Externalize() *HelmRepoExternal {
+	var serv integrations.IntegrationService
+
+	if hr.BasicAuthIntegrationID != 0 {
+		serv = integrations.HelmRepo
+	} else if hr.AWSIntegrationID != 0 {
+		serv = integrations.S3
+	} else if hr.GCPIntegrationID != 0 {
+		serv = integrations.GCS
+	}
+
+	return &HelmRepoExternal{
+		ID:        hr.ID,
+		ProjectID: hr.ProjectID,
+		Name:      hr.Name,
+		RepoURL:   hr.RepoURL,
+		Service:   serv,
+	}
+}

+ 56 - 0
internal/models/integrations/basic.go

@@ -0,0 +1,56 @@
+package integrations
+
+import "gorm.io/gorm"
+
+// BasicIntegration represents a basic auth mechanism via username/password
+type BasicIntegration struct {
+	gorm.Model
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// Username/Password for basic authentication to a cluster
+	Username []byte `json:"username,omitempty"`
+	Password []byte `json:"password,omitempty"`
+}
+
+// BasicIntegrationExternal is a BasicIntegration to be shared over REST
+type BasicIntegrationExternal struct {
+	ID uint `json:"id"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+}
+
+// Externalize generates an external BasicIntegration to be shared over REST
+func (b *BasicIntegration) Externalize() *BasicIntegrationExternal {
+	return &BasicIntegrationExternal{
+		ID:        b.ID,
+		UserID:    b.UserID,
+		ProjectID: b.ProjectID,
+	}
+}
+
+// ToProjectIntegration converts an oauth integration to a project integration
+func (b *BasicIntegration) ToProjectIntegration(
+	category string,
+	service IntegrationService,
+) *ProjectIntegration {
+	return &ProjectIntegration{
+		ID:            b.ID,
+		ProjectID:     b.ProjectID,
+		AuthMechanism: "basic",
+		Category:      category,
+		Service:       service,
+	}
+}

+ 31 - 9
internal/models/integrations/integration.go

@@ -5,13 +5,16 @@ type IntegrationService string
 
 // The list of supported third-party services
 const (
-	GKE    IntegrationService = "gke"
-	EKS    IntegrationService = "eks"
-	Kube   IntegrationService = "kube"
-	GCR    IntegrationService = "gcr"
-	ECR    IntegrationService = "ecr"
-	Github IntegrationService = "github"
-	Docker IntegrationService = "docker"
+	GKE      IntegrationService = "gke"
+	GCS      IntegrationService = "gcs"
+	S3       IntegrationService = "s3"
+	HelmRepo IntegrationService = "helm"
+	EKS      IntegrationService = "eks"
+	Kube     IntegrationService = "kube"
+	GCR      IntegrationService = "gcr"
+	ECR      IntegrationService = "ecr"
+	Github   IntegrationService = "github"
+	Docker   IntegrationService = "docker"
 )
 
 // PorterIntegration is a supported integration service, specifying an auth
@@ -74,8 +77,27 @@ var PorterRegistryIntegrations = []PorterIntegration{
 	},
 }
 
-// PorterRepoIntegrations are the supported repo integrations
-var PorterRepoIntegrations = []PorterIntegration{
+// PorterHelmRepoIntegrations are the supported helm repo integrations
+var PorterHelmRepoIntegrations = []PorterIntegration{
+	PorterIntegration{
+		AuthMechanism: "basic",
+		Category:      "helm",
+		Service:       HelmRepo,
+	},
+	PorterIntegration{
+		AuthMechanism: "gcp",
+		Category:      "helm",
+		Service:       GCS,
+	},
+	PorterIntegration{
+		AuthMechanism: "aws",
+		Category:      "helm",
+		Service:       S3,
+	},
+}
+
+// PorterGitRepoIntegrations are the supported git repo integrations
+var PorterGitRepoIntegrations = []PorterIntegration{
 	PorterIntegration{
 		AuthMechanism: "oauth",
 		Category:      "repo",

+ 1 - 1
internal/models/integrations/oauth.go

@@ -62,7 +62,7 @@ func (o *OAuthIntegration) Externalize() *OAuthIntegrationExternal {
 	}
 }
 
-// ToProjectIntegration converts a gcp integration to a project integration
+// ToProjectIntegration converts an oauth integration to a project integration
 func (o *OAuthIntegration) ToProjectIntegration(
 	category string,
 	service IntegrationService,

+ 21 - 29
internal/models/integrations/token_cache.go

@@ -6,23 +6,12 @@ import (
 	"gorm.io/gorm"
 )
 
-// GetTokenCacheFunc is a function that retrieves the token and expiry
-// time from the db
-type GetTokenCacheFunc func() (tok *TokenCache, err error)
-
-// SetTokenCacheFunc is a function that updates the token cache
-// with a new token and expiry time
-type SetTokenCacheFunc func(token string, expiry time.Time) error
-
 // TokenCache stores a token and an expiration for the token for a
 // service account. This will never be shared over REST, so no need
 // to externalize.
 type TokenCache struct {
 	gorm.Model
 
-	ClusterID  uint `json:"cluster_id"`
-	RegistryID uint `json:"registry_id"`
-
 	Expiry time.Time `json:"expiry,omitempty"`
 
 	// ------------------------------------------------------------------
@@ -32,37 +21,40 @@ type TokenCache struct {
 	Token []byte `json:"access_token"`
 }
 
+// GetTokenCacheFunc is a function that retrieves the token and expiry
+// time from the db
+type GetTokenCacheFunc func() (tok *TokenCache, err error)
+
+// SetTokenCacheFunc is a function that updates the token cache
+// with a new token and expiry time
+type SetTokenCacheFunc func(token string, expiry time.Time) error
+
 // IsExpired returns true if a token is expired, false otherwise
 func (t *TokenCache) IsExpired() bool {
 	return time.Now().After(t.Expiry)
 }
 
-// GetRegTokenCacheFunc is a function that retrieves the token and expiry
-// time from the db
-type GetRegTokenCacheFunc func() (tok *TokenCache, err error)
+// ClusterTokenCache is a token cache that clusters can use; a foreign
+// key constraint between a Cluster and ClusterTokenCache is created
+type ClusterTokenCache struct {
+	TokenCache
 
-// SetRegTokenCacheFunc is a function that updates the token cache
-// with a new token and expiry time
-type SetRegTokenCacheFunc func(token string, expiry time.Time) error
+	ClusterID uint `json:"cluster_id"`
+}
 
 // RegTokenCache stores a token and an expiration for the JWT token for a
 // Docker registry. This will never be shared over REST, so no need
 // to externalize.
 type RegTokenCache struct {
-	gorm.Model
+	TokenCache
 
 	RegistryID uint `json:"registry_id"`
-
-	Expiry time.Time `json:"expiry,omitempty"`
-
-	// ------------------------------------------------------------------
-	// All fields below this line are encrypted before storage
-	// ------------------------------------------------------------------
-
-	Token []byte `json:"access_token"`
 }
 
-// IsExpired returns true if a token is expired, false otherwise
-func (r *RegTokenCache) IsExpired() bool {
-	return time.Now().After(r.Expiry)
+// HelmRepoTokenCache is a token cache that helm repos can use; a foreign
+// key constraint between a HelmRepo and HelmRepoTokenCache is created
+type HelmRepoTokenCache struct {
+	TokenCache
+
+	HelmRepoID uint `json:"helm_repo_id"`
 }

+ 4 - 0
internal/models/project.go

@@ -23,8 +23,12 @@ type Project struct {
 	Clusters          []Cluster          `json:"clusters"`
 	ClusterCandidates []ClusterCandidate `json:"cluster_candidates"`
 
+	// linked helm repos
+	HelmRepos []HelmRepo `json:"helm_repos"`
+
 	// auth mechanisms
 	KubeIntegrations  []ints.KubeIntegration  `json:"kube_integrations"`
+	BasicIntegrations []ints.BasicIntegration `json:"basic_integrations"`
 	OIDCIntegrations  []ints.OIDCIntegration  `json:"oidc_integrations"`
 	OAuthIntegrations []ints.OAuthIntegration `json:"oauth_integrations"`
 	AWSIntegrations   []ints.AWSIntegration   `json:"aws_integrations"`

+ 6 - 5
internal/registry/registry.go

@@ -216,9 +216,8 @@ func (r *Registry) listECRRepositories(repo repository.Repository) ([]*Repositor
 
 func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
 	return &ints.TokenCache{
-		RegistryID: r.TokenCache.RegistryID,
-		Token:      r.TokenCache.Token,
-		Expiry:     r.TokenCache.Expiry,
+		Token:  r.TokenCache.Token,
+		Expiry: r.TokenCache.Expiry,
 	}, nil
 }
 
@@ -228,9 +227,11 @@ func (r *Registry) setTokenCacheFunc(
 	return func(token string, expiry time.Time) error {
 		_, err := repo.Registry.UpdateRegistryTokenCache(
 			&ints.RegTokenCache{
+				TokenCache: ints.TokenCache{
+					Token:  []byte(token),
+					Expiry: expiry,
+				},
 				RegistryID: r.ID,
-				Token:      []byte(token),
-				Expiry:     expiry,
 			},
 		)
 

+ 1 - 1
internal/repository/cluster.go

@@ -17,6 +17,6 @@ type ClusterRepository interface {
 	ReadCluster(id uint) (*models.Cluster, error)
 	ListClustersByProjectID(projectID uint) ([]*models.Cluster, error)
 	UpdateCluster(cluster *models.Cluster) (*models.Cluster, error)
-	UpdateClusterTokenCache(tokenCache *ints.TokenCache) (*models.Cluster, error)
+	UpdateClusterTokenCache(tokenCache *ints.ClusterTokenCache) (*models.Cluster, error)
 	DeleteCluster(cluster *models.Cluster) error
 }

+ 140 - 0
internal/repository/gorm/auth.go

@@ -228,6 +228,146 @@ func (repo *KubeIntegrationRepository) DecryptKubeIntegrationData(
 	return nil
 }
 
+// BasicIntegrationRepository uses gorm.DB for querying the database
+type BasicIntegrationRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewBasicIntegrationRepository returns a BasicIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewBasicIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+) repository.BasicIntegrationRepository {
+	return &BasicIntegrationRepository{db, key}
+}
+
+// CreateBasicIntegration creates a new basic auth mechanism
+func (repo *BasicIntegrationRepository) CreateBasicIntegration(
+	am *ints.BasicIntegration,
+) (*ints.BasicIntegration, error) {
+	err := repo.EncryptBasicIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("BasicIntegrations")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(am); err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
+// ReadBasicIntegration finds a basic auth mechanism by id
+func (repo *BasicIntegrationRepository) ReadBasicIntegration(
+	id uint,
+) (*ints.BasicIntegration, error) {
+	basic := &ints.BasicIntegration{}
+
+	if err := repo.db.Where("id = ?", id).First(&basic).Error; err != nil {
+		return nil, err
+	}
+
+	err := repo.DecryptBasicIntegrationData(basic, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return basic, nil
+}
+
+// ListBasicIntegrationsByProjectID finds all basic auth mechanisms
+// for a given project id
+func (repo *BasicIntegrationRepository) ListBasicIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.BasicIntegration, error) {
+	basics := []*ints.BasicIntegration{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&basics).Error; err != nil {
+		return nil, err
+	}
+
+	for _, basic := range basics {
+		repo.DecryptBasicIntegrationData(basic, repo.key)
+	}
+
+	return basics, nil
+}
+
+// EncryptBasicIntegrationData will encrypt the basic integration data before
+// writing to the DB
+func (repo *BasicIntegrationRepository) EncryptBasicIntegrationData(
+	basic *ints.BasicIntegration,
+	key *[32]byte,
+) error {
+	if len(basic.Username) > 0 {
+		cipherData, err := repository.Encrypt(basic.Username, key)
+
+		if err != nil {
+			return err
+		}
+
+		basic.Username = cipherData
+	}
+
+	if len(basic.Password) > 0 {
+		cipherData, err := repository.Encrypt(basic.Password, key)
+
+		if err != nil {
+			return err
+		}
+
+		basic.Password = cipherData
+	}
+
+	return nil
+}
+
+// DecryptBasicIntegrationData will decrypt the basic integration data before
+// returning it from the DB
+func (repo *BasicIntegrationRepository) DecryptBasicIntegrationData(
+	basic *ints.BasicIntegration,
+	key *[32]byte,
+) error {
+	if len(basic.Username) > 0 {
+		plaintext, err := repository.Decrypt(basic.Username, key)
+
+		if err != nil {
+			return err
+		}
+
+		basic.Username = plaintext
+	}
+
+	if len(basic.Password) > 0 {
+		plaintext, err := repository.Decrypt(basic.Password, key)
+
+		if err != nil {
+			return err
+		}
+
+		basic.Password = plaintext
+	}
+
+	return nil
+}
+
 // OIDCIntegrationRepository uses gorm.DB for querying the database
 type OIDCIntegrationRepository struct {
 	db  *gorm.DB

+ 86 - 0
internal/repository/gorm/auth_test.go

@@ -94,6 +94,92 @@ func TestListKubeIntegrationsByProjectID(t *testing.T) {
 	}
 }
 
+func TestCreateBasicIntegration(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_basic.db",
+	}
+
+	setupTestEnv(tester, t)
+	initUser(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	basic := &ints.BasicIntegration{
+		ProjectID: tester.initProjects[0].ID,
+		UserID:    tester.initUsers[0].ID,
+		Username:  []byte("username"),
+		Password:  []byte("password"),
+	}
+
+	expBasic := *basic
+
+	basic, err := tester.repo.BasicIntegration.CreateBasicIntegration(basic)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	basic, err = tester.repo.BasicIntegration.ReadBasicIntegration(basic.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if basic.Model.ID != 1 {
+		t.Errorf("incorrect basic integration ID: expected %d, got %d\n", 1, basic.Model.ID)
+	}
+
+	// reset fields for deep.Equal
+	basic.Model = orm.Model{}
+
+	if diff := deep.Equal(expBasic, *basic); diff != nil {
+		t.Errorf("incorrect basic integration")
+		t.Error(diff)
+	}
+}
+
+func TestListBasicIntegrationsByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_basics.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initBasicIntegration(tester, t)
+	defer cleanup(tester, t)
+
+	basics, err := tester.repo.BasicIntegration.ListBasicIntegrationsByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(basics) != 1 {
+		t.Fatalf("length of basic integrations incorrect: expected %d, got %d\n", 1, len(basics))
+	}
+
+	// make sure data is correct
+	expBasic := ints.BasicIntegration{
+		ProjectID: tester.initProjects[0].ID,
+		UserID:    tester.initUsers[0].ID,
+		Username:  []byte("username"),
+		Password:  []byte("password"),
+	}
+
+	basic := basics[0]
+
+	// reset fields for reflect.DeepEqual
+	basic.Model = orm.Model{}
+
+	if diff := deep.Equal(expBasic, *basic); diff != nil {
+		t.Errorf("incorrect basic integration")
+		t.Error(diff)
+	}
+}
+
 func TestCreateOIDCIntegration(t *testing.T) {
 	tester := &tester{
 		dbFileName: "./porter_create_oidc.db",

+ 1 - 1
internal/repository/gorm/cluster.go

@@ -206,7 +206,7 @@ func (repo *ClusterRepository) UpdateCluster(
 
 // UpdateClusterTokenCache updates the token cache for a cluster
 func (repo *ClusterRepository) UpdateClusterTokenCache(
-	tokenCache *ints.TokenCache,
+	tokenCache *ints.ClusterTokenCache,
 ) (*models.Cluster, error) {
 	if tok := tokenCache.Token; len(tok) > 0 {
 		cipherData, err := repository.Encrypt(tok, repo.key)

+ 5 - 3
internal/repository/gorm/cluster_test.go

@@ -357,9 +357,11 @@ func TestUpdateClusterToken(t *testing.T) {
 		Server:                   "https://localhost",
 		KubeIntegrationID:        tester.initKIs[0].ID,
 		CertificateAuthorityData: []byte("-----BEGIN"),
-		TokenCache: ints.TokenCache{
-			Token:  []byte("token-1"),
-			Expiry: time.Now().Add(-1 * time.Hour),
+		TokenCache: ints.ClusterTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
 		},
 	}
 

+ 196 - 0
internal/repository/gorm/helm_repo.go

@@ -0,0 +1,196 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// HelmRepoRepository uses gorm.DB for querying the database
+type HelmRepoRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewHelmRepoRepository returns a HelmRepoRepository which uses
+// gorm.DB for querying the database
+func NewHelmRepoRepository(db *gorm.DB, key *[32]byte) repository.HelmRepoRepository {
+	return &HelmRepoRepository{db, key}
+}
+
+// CreateHelmRepo creates a new helm repo
+func (repo *HelmRepoRepository) CreateHelmRepo(hr *models.HelmRepo) (*models.HelmRepo, error) {
+	err := repo.EncryptHelmRepoData(hr, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", hr.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("HelmRepos")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(hr); err != nil {
+		return nil, err
+	}
+
+	// create a token cache by default
+	assoc = repo.db.Model(hr).Association("TokenCache")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(&hr.TokenCache); err != nil {
+		return nil, err
+	}
+
+	err = repo.DecryptHelmRepoData(hr, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return hr, nil
+}
+
+// ReadHelmRepo gets a helm repo specified by a unique id
+func (repo *HelmRepoRepository) ReadHelmRepo(id uint) (*models.HelmRepo, error) {
+	hr := &models.HelmRepo{}
+
+	if err := repo.db.Preload("TokenCache").Where("id = ?", id).First(&hr).Error; err != nil {
+		return nil, err
+	}
+
+	repo.DecryptHelmRepoData(hr, repo.key)
+
+	return hr, nil
+}
+
+// ListHelmReposByProjectID finds all helm repos
+// for a given project id
+func (repo *HelmRepoRepository) ListHelmReposByProjectID(
+	projectID uint,
+) ([]*models.HelmRepo, error) {
+	hrs := []*models.HelmRepo{}
+
+	if err := repo.db.Preload("TokenCache").Where("project_id = ?", projectID).Find(&hrs).Error; err != nil {
+		return nil, err
+	}
+
+	for _, hr := range hrs {
+		repo.DecryptHelmRepoData(hr, repo.key)
+	}
+
+	return hrs, nil
+}
+
+// UpdateHelmRepo modifies an existing HelmRepo in the database
+func (repo *HelmRepoRepository) UpdateHelmRepo(
+	hr *models.HelmRepo,
+) (*models.HelmRepo, error) {
+	if err := repo.db.Save(hr).Error; err != nil {
+		return nil, err
+	}
+
+	return hr, nil
+}
+
+// UpdateHelmRepoTokenCache updates the helm repo for a registry
+func (repo *HelmRepoRepository) UpdateHelmRepoTokenCache(
+	tokenCache *ints.HelmRepoTokenCache,
+) (*models.HelmRepo, error) {
+	if tok := tokenCache.Token; len(tok) > 0 {
+		cipherData, err := repository.Encrypt(tok, repo.key)
+
+		if err != nil {
+			return nil, err
+		}
+
+		tokenCache.Token = cipherData
+	}
+
+	hr := &models.HelmRepo{}
+
+	if err := repo.db.Where("id = ?", tokenCache.HelmRepoID).First(&hr).Error; err != nil {
+		return nil, err
+	}
+
+	hr.TokenCache.Token = tokenCache.Token
+	hr.TokenCache.Expiry = tokenCache.Expiry
+
+	if err := repo.db.Save(hr).Error; err != nil {
+		return nil, err
+	}
+
+	return hr, nil
+}
+
+// DeleteHelmRepo removes a registry from the db
+func (repo *HelmRepoRepository) DeleteHelmRepo(
+	hr *models.HelmRepo,
+) error {
+	// clear TokenCache association
+	assoc := repo.db.Model(hr).Association("TokenCache")
+
+	if assoc.Error != nil {
+		return assoc.Error
+	}
+
+	if err := assoc.Clear(); err != nil {
+		return err
+	}
+
+	if err := repo.db.Where("id = ?", hr.ID).Delete(&models.HelmRepo{}).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// EncryptHelmRepoData will encrypt the user's helm repo data before writing
+// to the DB
+func (repo *HelmRepoRepository) EncryptHelmRepoData(
+	hr *models.HelmRepo,
+	key *[32]byte,
+) error {
+	if tok := hr.TokenCache.Token; len(tok) > 0 {
+		cipherData, err := repository.Encrypt(tok, key)
+
+		if err != nil {
+			return err
+		}
+
+		hr.TokenCache.Token = cipherData
+	}
+
+	return nil
+}
+
+// DecryptHelmRepoData will decrypt the user's helm repo data before returning it
+// from the DB
+func (repo *HelmRepoRepository) DecryptHelmRepoData(
+	hr *models.HelmRepo,
+	key *[32]byte,
+) error {
+	if tok := hr.TokenCache.Token; len(tok) > 0 {
+		plaintext, err := repository.Decrypt(tok, key)
+
+		if err != nil {
+			return err
+		}
+
+		hr.TokenCache.Token = plaintext
+	}
+
+	return nil
+}

+ 250 - 0
internal/repository/gorm/helm_repo_test.go

@@ -0,0 +1,250 @@
+package gorm_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
+	orm "gorm.io/gorm"
+)
+
+func TestCreateHelmRepo(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_hr.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	hr := &models.HelmRepo{
+		Name:      "helm-repo-test",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	hr, err := tester.repo.HelmRepo.CreateHelmRepo(hr)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	hr, err = tester.repo.HelmRepo.ReadHelmRepo(hr.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1 and name is "registry-test"
+	if hr.Model.ID != 1 {
+		t.Errorf("incorrect registry ID: expected %d, got %d\n", 1, hr.Model.ID)
+	}
+
+	if hr.Name != "helm-repo-test" {
+		t.Errorf("incorrect helm repo name: expected %s, got %s\n", "helm-repo-test", hr.Name)
+	}
+
+	if hr.RepoURL != "https://example-repo.com" {
+		t.Errorf("incorrect helm repo url: expected %s, got %s\n", "https://example-repo.com", hr.RepoURL)
+	}
+}
+
+func TestListHelmReposByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_hrs.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initHelmRepo(tester, t)
+	defer cleanup(tester, t)
+
+	hrs, err := tester.repo.HelmRepo.ListHelmReposByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(hrs) != 1 {
+		t.Fatalf("length of helm repos incorrect: expected %d, got %d\n", 1, len(hrs))
+	}
+
+	// make sure data is correct
+	expHelmRepo := models.HelmRepo{
+		Name:      "helm-repo-test",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	hr := hrs[0]
+
+	// reset fields for reflect.DeepEqual
+	hr.Model = gorm.Model{}
+
+	if diff := deep.Equal(expHelmRepo, *hr); diff != nil {
+		t.Errorf("incorrect helm repo")
+		t.Error(diff)
+	}
+}
+
+func TestUpdateHelmRepo(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_update_hr.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initHelmRepo(tester, t)
+	defer cleanup(tester, t)
+
+	hr := tester.initHRs[0]
+
+	hr.Name = "helm-repo-new-name"
+
+	hr, err := tester.repo.HelmRepo.UpdateHelmRepo(
+		hr,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	hr, err = tester.repo.HelmRepo.ReadHelmRepo(tester.initHRs[0].ID)
+
+	// make sure data is correct
+	expHelmRepo := models.HelmRepo{
+		Name:      "helm-repo-new-name",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	// reset fields for reflect.DeepEqual
+	hr.Model = orm.Model{}
+
+	if diff := deep.Equal(expHelmRepo, *hr); diff != nil {
+		t.Errorf("incorrect helm repo")
+		t.Error(diff)
+	}
+}
+
+func TestUpdateHelmRepoToken(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_test_update_hr_token.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	hr := &models.HelmRepo{
+		Name:      "helm-repo-test",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+		TokenCache: ints.HelmRepoTokenCache{
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
+		},
+	}
+
+	hr, err := tester.repo.HelmRepo.CreateHelmRepo(hr)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	hr, err = tester.repo.HelmRepo.ReadHelmRepo(hr.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure helm repo id of token is 1
+	if hr.TokenCache.HelmRepoID != 1 {
+		t.Fatalf("incorrect helm repo id in token cache: expected %d, got %d\n", 1, hr.TokenCache.HelmRepoID)
+	}
+
+	// make sure old token is expired
+	if isExpired := hr.TokenCache.IsExpired(); !isExpired {
+		t.Fatalf("token was not expired\n")
+	}
+
+	if string(hr.TokenCache.Token) != "token-1" {
+		t.Errorf("incorrect token in cache: expected %s, got %s\n", "token-1", hr.TokenCache.Token)
+	}
+
+	hr.TokenCache.Token = []byte("token-2")
+	hr.TokenCache.Expiry = time.Now().Add(24 * time.Hour)
+
+	hr, err = tester.repo.HelmRepo.UpdateHelmRepoTokenCache(&hr.TokenCache)
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+	hr, err = tester.repo.HelmRepo.ReadHelmRepo(hr.Model.ID)
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if hr.Model.ID != 1 {
+		t.Errorf("incorrect helm repo ID: expected %d, got %d\n", 1, hr.Model.ID)
+	}
+
+	// make sure new token is correct and not expired
+	if hr.TokenCache.HelmRepoID != 1 {
+		t.Fatalf("incorrect helm repo ID in token cache: expected %d, got %d\n", 1, hr.TokenCache.HelmRepoID)
+	}
+
+	if isExpired := hr.TokenCache.IsExpired(); isExpired {
+		t.Fatalf("token was expired\n")
+	}
+
+	if string(hr.TokenCache.Token) != "token-2" {
+		t.Errorf("incorrect token in cache: expected %s, got %s\n", "token-2", hr.TokenCache.Token)
+	}
+}
+
+// func TestDeleteRegistry(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_delete_registry.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	initRegistry(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	reg, err := tester.repo.Registry.ReadRegistry(tester.initRegs[0].Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	err = tester.repo.Registry.DeleteRegistry(reg)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	_, err = tester.repo.Registry.ReadRegistry(tester.initRegs[0].Model.ID)
+
+// 	if err != orm.ErrRecordNotFound {
+// 		t.Fatalf("incorrect error: expected %v, got %v\n", orm.ErrRecordNotFound, err)
+// 	}
+
+// 	regs, err := tester.repo.Registry.ListRegistriesByProjectID(tester.initProjects[0].Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	if len(regs) != 0 {
+// 		t.Fatalf("length of clusters was not 0")
+// 	}
+// }

+ 55 - 1
internal/repository/gorm/helpers_test.go

@@ -21,8 +21,10 @@ type tester struct {
 	initGRs      []*models.GitRepo
 	initRegs     []*models.Registry
 	initClusters []*models.Cluster
+	initHRs      []*models.HelmRepo
 	initCCs      []*models.ClusterCandidate
 	initKIs      []*ints.KubeIntegration
+	initBasics   []*ints.BasicIntegration
 	initOIDCs    []*ints.OIDCIntegration
 	initOAuths   []*ints.OAuthIntegration
 	initGCPs     []*ints.GCPIntegration
@@ -49,16 +51,19 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.Session{},
 		&models.GitRepo{},
 		&models.Registry{},
+		&models.HelmRepo{},
 		&models.Cluster{},
 		&models.ClusterCandidate{},
 		&models.ClusterResolver{},
 		&ints.KubeIntegration{},
+		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},
 		&ints.OAuthIntegration{},
 		&ints.GCPIntegration{},
 		&ints.AWSIntegration{},
-		&ints.TokenCache{},
+		&ints.ClusterTokenCache{},
 		&ints.RegTokenCache{},
+		&ints.HelmRepoTokenCache{},
 	)
 
 	if err != nil {
@@ -159,6 +164,33 @@ func initKubeIntegration(tester *tester, t *testing.T) {
 	tester.initKIs = append(tester.initKIs, ki)
 }
 
+func initBasicIntegration(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	basic := &ints.BasicIntegration{
+		ProjectID: tester.initProjects[0].ID,
+		UserID:    tester.initUsers[0].ID,
+		Username:  []byte("username"),
+		Password:  []byte("password"),
+	}
+
+	basic, err := tester.repo.BasicIntegration.CreateBasicIntegration(basic)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initBasics = append(tester.initBasics, basic)
+}
+
 func initOIDCIntegration(tester *tester, t *testing.T) {
 	t.Helper()
 
@@ -380,3 +412,25 @@ func initRegistry(tester *tester, t *testing.T) {
 
 	tester.initRegs = append(tester.initRegs, reg)
 }
+
+func initHelmRepo(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	hr := &models.HelmRepo{
+		Name:      "helm-repo-test",
+		RepoURL:   "https://example-repo.com",
+		ProjectID: tester.initProjects[0].Model.ID,
+	}
+
+	hr, err := tester.repo.HelmRepo.CreateHelmRepo(hr)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initHRs = append(tester.initHRs, hr)
+}

+ 4 - 2
internal/repository/gorm/registry_test.go

@@ -138,8 +138,10 @@ func TestUpdateRegistryToken(t *testing.T) {
 		Name:      "registry-test",
 		ProjectID: tester.initProjects[0].Model.ID,
 		TokenCache: ints.RegTokenCache{
-			Token:  []byte("token-1"),
-			Expiry: time.Now().Add(-1 * time.Hour),
+			TokenCache: ints.TokenCache{
+				Token:  []byte("token-1"),
+				Expiry: time.Now().Add(-1 * time.Hour),
+			},
 		},
 	}
 

+ 2 - 0
internal/repository/gorm/repository.go

@@ -14,8 +14,10 @@ func NewRepository(db *gorm.DB, key *[32]byte) *repository.Repository {
 		Project:          NewProjectRepository(db),
 		GitRepo:          NewGitRepoRepository(db, key),
 		Cluster:          NewClusterRepository(db, key),
+		HelmRepo:         NewHelmRepoRepository(db, key),
 		Registry:         NewRegistryRepository(db, key),
 		KubeIntegration:  NewKubeIntegrationRepository(db, key),
+		BasicIntegration: NewBasicIntegrationRepository(db, key),
 		OIDCIntegration:  NewOIDCIntegrationRepository(db, key),
 		OAuthIntegration: NewOAuthIntegrationRepository(db, key),
 		GCPIntegration:   NewGCPIntegrationRepository(db, key),

+ 16 - 0
internal/repository/helm_repo.go

@@ -0,0 +1,16 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// HelmRepoRepository represents the set of queries on the HelmRepo model
+type HelmRepoRepository interface {
+	CreateHelmRepo(repo *models.HelmRepo) (*models.HelmRepo, error)
+	ReadHelmRepo(id uint) (*models.HelmRepo, error)
+	ListHelmReposByProjectID(projectID uint) ([]*models.HelmRepo, error)
+	UpdateHelmRepo(repo *models.HelmRepo) (*models.HelmRepo, error)
+	UpdateHelmRepoTokenCache(tokenCache *ints.HelmRepoTokenCache) (*models.HelmRepo, error)
+	DeleteHelmRepo(repo *models.HelmRepo) error
+}

+ 8 - 0
internal/repository/integrations.go

@@ -12,6 +12,14 @@ type KubeIntegrationRepository interface {
 	ListKubeIntegrationsByProjectID(projectID uint) ([]*ints.KubeIntegration, error)
 }
 
+// BasicIntegrationRepository represents the set of queries on the "basic" auth
+// mechanism
+type BasicIntegrationRepository interface {
+	CreateBasicIntegration(am *ints.BasicIntegration) (*ints.BasicIntegration, error)
+	ReadBasicIntegration(id uint) (*ints.BasicIntegration, error)
+	ListBasicIntegrationsByProjectID(projectID uint) ([]*ints.BasicIntegration, error)
+}
+
 // OIDCIntegrationRepository represents the set of queries on the OIDC auth
 // mechanism
 type OIDCIntegrationRepository interface {

+ 2 - 0
internal/repository/repository.go

@@ -7,8 +7,10 @@ type Repository struct {
 	Session          SessionRepository
 	GitRepo          GitRepoRepository
 	Cluster          ClusterRepository
+	HelmRepo         HelmRepoRepository
 	Registry         RegistryRepository
 	KubeIntegration  KubeIntegrationRepository
+	BasicIntegration BasicIntegrationRepository
 	OIDCIntegration  OIDCIntegrationRepository
 	OAuthIntegration OAuthIntegrationRepository
 	GCPIntegration   GCPIntegrationRepository

+ 64 - 0
internal/repository/test/auth.go

@@ -73,6 +73,70 @@ func (repo *KubeIntegrationRepository) ListKubeIntegrationsByProjectID(
 	return res, nil
 }
 
+// BasicIntegrationRepository implements repository.BasicIntegrationRepository
+type BasicIntegrationRepository struct {
+	canQuery          bool
+	basicIntegrations []*ints.BasicIntegration
+}
+
+// NewBasicIntegrationRepository will return errors if canQuery is false
+func NewBasicIntegrationRepository(canQuery bool) repository.BasicIntegrationRepository {
+	return &BasicIntegrationRepository{
+		canQuery,
+		[]*ints.BasicIntegration{},
+	}
+}
+
+// CreateBasicIntegration creates a new basic auth mechanism
+func (repo *BasicIntegrationRepository) CreateBasicIntegration(
+	am *ints.BasicIntegration,
+) (*ints.BasicIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.basicIntegrations = append(repo.basicIntegrations, am)
+	am.ID = uint(len(repo.basicIntegrations))
+
+	return am, nil
+}
+
+// ReadBasicIntegration finds a basic auth mechanism by id
+func (repo *BasicIntegrationRepository) ReadBasicIntegration(
+	id uint,
+) (*ints.BasicIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.basicIntegrations) || repo.basicIntegrations[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.basicIntegrations[index], nil
+}
+
+// ListBasicIntegrationsByProjectID finds all basic auth mechanisms
+// for a given project id
+func (repo *BasicIntegrationRepository) ListBasicIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.BasicIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*ints.BasicIntegration, 0)
+
+	for _, basicAM := range repo.basicIntegrations {
+		if basicAM.ProjectID == projectID {
+			res = append(res, basicAM)
+		}
+	}
+
+	return res, nil
+}
+
 // OIDCIntegrationRepository implements repository.OIDCIntegrationRepository
 type OIDCIntegrationRepository struct {
 	canQuery         bool

+ 1 - 1
internal/repository/test/cluster.go

@@ -164,7 +164,7 @@ func (repo *ClusterRepository) UpdateCluster(
 
 // UpdateClusterTokenCache updates the token cache for a cluster
 func (repo *ClusterRepository) UpdateClusterTokenCache(
-	tokenCache *ints.TokenCache,
+	tokenCache *ints.ClusterTokenCache,
 ) (*models.Cluster, error) {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot write database")

+ 125 - 0
internal/repository/test/helm_repo.go

@@ -0,0 +1,125 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// HelmRepoRepository implements repository.HelmRepoRepository
+type HelmRepoRepository struct {
+	canQuery  bool
+	helmRepos []*models.HelmRepo
+}
+
+// NewHelmRepoRepository will return errors if canQuery is false
+func NewHelmRepoRepository(canQuery bool) repository.HelmRepoRepository {
+	return &HelmRepoRepository{
+		canQuery,
+		[]*models.HelmRepo{},
+	}
+}
+
+// CreateHelmRepo creates a new repoistry
+func (repo *HelmRepoRepository) CreateHelmRepo(
+	hr *models.HelmRepo,
+) (*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.helmRepos = append(repo.helmRepos, hr)
+	hr.ID = uint(len(repo.helmRepos))
+
+	return hr, nil
+}
+
+// ReadHelmRepo finds a repoistry by id
+func (repo *HelmRepoRepository) ReadHelmRepo(
+	id uint,
+) (*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.helmRepos) || repo.helmRepos[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.helmRepos[index], nil
+}
+
+// ListHelmReposByProjectID finds all repoistries
+// for a given project id
+func (repo *HelmRepoRepository) ListHelmReposByProjectID(
+	projectID uint,
+) ([]*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*models.HelmRepo, 0)
+
+	for _, hr := range repo.helmRepos {
+		if hr != nil && hr.ProjectID == projectID {
+			res = append(res, hr)
+		}
+	}
+
+	return res, nil
+}
+
+// UpdateHelmRepo modifies an existing HelmRepo in the database
+func (repo *HelmRepoRepository) UpdateHelmRepo(
+	hr *models.HelmRepo,
+) (*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if int(hr.ID-1) >= len(repo.helmRepos) || repo.helmRepos[hr.ID-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(hr.ID - 1)
+	repo.helmRepos[index] = hr
+
+	return hr, nil
+}
+
+// UpdateHelmRepoTokenCache updates the token cache for a repoistry
+func (repo *HelmRepoRepository) UpdateHelmRepoTokenCache(
+	tokenCache *ints.HelmRepoTokenCache,
+) (*models.HelmRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	index := int(tokenCache.HelmRepoID - 1)
+	repo.helmRepos[index].TokenCache.Token = tokenCache.Token
+	repo.helmRepos[index].TokenCache.Expiry = tokenCache.Expiry
+
+	return repo.helmRepos[index], nil
+}
+
+// DeleteHelmRepo removes a repoistry from the array by setting it to nil
+func (repo *HelmRepoRepository) DeleteHelmRepo(
+	hr *models.HelmRepo,
+) error {
+	if !repo.canQuery {
+		return errors.New("Cannot write database")
+	}
+
+	if int(hr.ID-1) >= len(repo.helmRepos) || repo.helmRepos[hr.ID-1] == nil {
+		return gorm.ErrRecordNotFound
+	}
+
+	index := int(hr.ID - 1)
+	repo.helmRepos[index] = nil
+
+	return nil
+}

+ 2 - 0
internal/repository/test/repository.go

@@ -12,9 +12,11 @@ func NewRepository(canQuery bool) *repository.Repository {
 		Session:          NewSessionRepository(canQuery),
 		Project:          NewProjectRepository(canQuery),
 		Cluster:          NewClusterRepository(canQuery),
+		HelmRepo:         NewHelmRepoRepository(canQuery),
 		Registry:         NewRegistryRepository(canQuery),
 		GitRepo:          NewGitRepoRepository(canQuery),
 		KubeIntegration:  NewKubeIntegrationRepository(canQuery),
+		BasicIntegration: NewBasicIntegrationRepository(canQuery),
 		OIDCIntegration:  NewOIDCIntegrationRepository(canQuery),
 		OAuthIntegration: NewOAuthIntegrationRepository(canQuery),
 		GCPIntegration:   NewGCPIntegrationRepository(canQuery),

+ 1 - 1
server/api/cluster_handler_test.go

@@ -250,7 +250,7 @@ var createProjectClusterCandidatesTests = []*clusterTest{
 					Name:                     "cluster-test",
 					Server:                   "https://10.10.10.10",
 					OIDCIntegrationID:        1,
-					TokenCache:               integrations.TokenCache{},
+					TokenCache:               integrations.ClusterTokenCache{},
 					CertificateAuthorityData: []byte("-----BEGIN CER"),
 				}
 

+ 1 - 1
server/api/deploy_handler.go

@@ -37,7 +37,7 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 
 	getChartForm.PopulateRepoURLFromQueryParams(vals)
 
-	chart, err := loader.LoadChart(getChartForm.RepoURL, getChartForm.Name, getChartForm.Version)
+	chart, err := loader.LoadChartPublic(getChartForm.RepoURL, getChartForm.Name, getChartForm.Version)
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)

+ 131 - 0
server/api/helm_repo_handler.go

@@ -0,0 +1,131 @@
+package api
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"github.com/porter-dev/porter/internal/helm/repo"
+
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// HandleCreateHelmRepo creates a new helm repo for a project
+func (app *App) HandleCreateHelmRepo(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateHelmRepo{
+		ProjectID: uint(projID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to a registry
+	hr, err := form.ToHelmRepo()
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	hr, err = app.repo.HelmRepo.CreateHelmRepo(hr)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	app.logger.Info().Msgf("New helm repo created: %d", hr.ID)
+
+	w.WriteHeader(http.StatusCreated)
+
+	hrExt := hr.Externalize()
+
+	if err := json.NewEncoder(w).Encode(hrExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// HandleListProjectHelmRepos returns a list of helm repos for a project
+func (app *App) HandleListProjectHelmRepos(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	hrs, err := app.repo.HelmRepo.ListHelmReposByProjectID(uint(projID))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	extHRs := make([]*models.HelmRepoExternal, 0)
+
+	for _, hr := range hrs {
+		extHRs = append(extHRs, hr.Externalize())
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(extHRs); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// HandleListHelmRepoCharts lists the charts for a given linked helm repo
+func (app *App) HandleListHelmRepoCharts(w http.ResponseWriter, r *http.Request) {
+	helmID, err := strconv.ParseUint(chi.URLParam(r, "helm_id"), 0, 64)
+
+	if err != nil || helmID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	hr, err := app.repo.HelmRepo.ReadHelmRepo(uint(helmID))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	// cast to a registry from registry package
+	_hr := repo.HelmRepo(*hr)
+	hrAPI := &_hr
+
+	charts, err := hrAPI.ListCharts(*app.repo)
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(charts); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}

+ 157 - 0
server/api/helm_repo_handler_test.go

@@ -0,0 +1,157 @@
+package api_test
+
+import (
+	"encoding/json"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
+
+type helmTest struct {
+	initializers []func(t *tester)
+	msg          string
+	method       string
+	endpoint     string
+	body         string
+	expStatus    int
+	expBody      string
+	useCookie    bool
+	validators   []func(c *helmTest, tester *tester, t *testing.T)
+}
+
+func testHelmRepoRequests(t *testing.T, tests []*helmTest, canQuery bool) {
+	for _, c := range tests {
+		// create a new tester
+		tester := newTester(canQuery)
+
+		// if there's an initializer, call it
+		for _, init := range c.initializers {
+			init(tester)
+		}
+
+		req, err := http.NewRequest(
+			c.method,
+			c.endpoint,
+			strings.NewReader(c.body),
+		)
+
+		tester.req = req
+
+		if c.useCookie {
+			req.AddCookie(tester.cookie)
+		}
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		tester.execute()
+		rr := tester.rr
+
+		// first, check that the status matches
+		if status := rr.Code; status != c.expStatus {
+			t.Errorf("%s, handler returned wrong status code: got %v want %v",
+				c.msg, status, c.expStatus)
+		}
+
+		// if there's a validator, call it
+		for _, validate := range c.validators {
+			validate(c, tester, t)
+		}
+	}
+}
+
+// ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
+
+var createHelmRepoTests = []*helmTest{
+	&helmTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+		},
+		msg:       "Create helm repo",
+		method:    "POST",
+		endpoint:  "/api/projects/1/helmrepos",
+		body:      `{"name":"helm-repo-test","basic_integration_id":1,"repo_url":"https://example-repo.com"}`,
+		expStatus: http.StatusCreated,
+		expBody:   `{"id":1,"project_id":1,"name":"helm-repo-test","repo_name":"https://example-repo.com","service":"helm"}`,
+		useCookie: true,
+		validators: []func(c *helmTest, tester *tester, t *testing.T){
+			helmRepoBodyValidator,
+		},
+	},
+}
+
+func TestHandleCreateHelmRepo(t *testing.T) {
+	testHelmRepoRequests(t, createHelmRepoTests, true)
+}
+
+var listHelmReposTest = []*helmTest{
+	&helmTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+			initHelmRepo,
+		},
+		msg:       "List helm repos",
+		method:    "GET",
+		endpoint:  "/api/projects/1/helmrepos",
+		body:      ``,
+		expStatus: http.StatusOK,
+		expBody:   `[{"id":1,"project_id":1,"name":"helm-repo-test","repo_name":"https://example-repo.com","service":"helm"}]`,
+		useCookie: true,
+		validators: []func(c *helmTest, tester *tester, t *testing.T){
+			hrsBodyValidator,
+		},
+	},
+}
+
+func TestHandleListHelmRepos(t *testing.T) {
+	testHelmRepoRequests(t, listHelmReposTest, true)
+}
+
+// ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
+
+func initHelmRepo(tester *tester) {
+	proj, _ := tester.repo.Project.ReadProject(1)
+
+	hr := &models.HelmRepo{
+		Name:                   "helm-repo-test",
+		ProjectID:              proj.Model.ID,
+		RepoURL:                "https://example-repo.com",
+		BasicAuthIntegrationID: 1,
+	}
+
+	tester.repo.HelmRepo.CreateHelmRepo(hr)
+}
+
+func helmRepoBodyValidator(c *helmTest, tester *tester, t *testing.T) {
+	gotBody := &models.HelmRepoExternal{}
+	expBody := &models.HelmRepoExternal{}
+
+	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
+	json.Unmarshal([]byte(c.expBody), &expBody)
+
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
+	}
+}
+
+func hrsBodyValidator(c *helmTest, tester *tester, t *testing.T) {
+	gotBody := make([]*models.HelmRepoExternal, 0)
+	expBody := make([]*models.HelmRepoExternal, 0)
+
+	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
+	json.Unmarshal([]byte(c.expBody), &expBody)
+
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
+	}
+}

+ 77 - 1
server/api/integration_handler.go

@@ -37,10 +37,23 @@ func (app *App) HandleListRegistryIntegrations(w http.ResponseWriter, r *http.Re
 	}
 }
 
+// HandleListHelmRepoIntegrations lists the Helm repo integrations available to the
+// instance
+func (app *App) HandleListHelmRepoIntegrations(w http.ResponseWriter, r *http.Request) {
+	hrs := ints.PorterHelmRepoIntegrations
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(&hrs); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 // HandleListRepoIntegrations lists the repo integrations available to the
 // instance
 func (app *App) HandleListRepoIntegrations(w http.ResponseWriter, r *http.Request) {
-	repos := ints.PorterRepoIntegrations
+	repos := ints.PorterGitRepoIntegrations
 
 	w.WriteHeader(http.StatusOK)
 
@@ -175,3 +188,66 @@ func (app *App) HandleCreateAWSIntegration(w http.ResponseWriter, r *http.Reques
 		return
 	}
 }
+
+// HandleCreateBasicAuthIntegration creates a new basic auth integration in the DB
+func (app *App) HandleCreateBasicAuthIntegration(w http.ResponseWriter, r *http.Request) {
+	session, err := app.store.Get(r, app.cookieName)
+
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateBasicAuthIntegrationForm{
+		UserID:    userID,
+		ProjectID: uint(projID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to a gcp integration
+	basic, err := form.ToBasicIntegration()
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	basic, err = app.repo.BasicIntegration.CreateBasicIntegration(basic)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	app.logger.Info().Msgf("New basic integration created: %d", basic.ID)
+
+	w.WriteHeader(http.StatusCreated)
+
+	basicExt := basic.Externalize()
+
+	if err := json.NewEncoder(w).Encode(basicExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}

+ 71 - 1
server/api/integration_handler_test.go

@@ -2,6 +2,7 @@ package api_test
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"strings"
 	"testing"
@@ -112,6 +113,28 @@ func TestHandleListRegistryIntegrations(t *testing.T) {
 	testPublicIntegrationRequests(t, listRegistryIntegrationsTests, true)
 }
 
+var listHelmRepoIntegrationsTest = []*publicIntTest{
+	&publicIntTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+		},
+		msg:       "List Helm repo integrations",
+		method:    "GET",
+		endpoint:  "/api/integrations/helm",
+		body:      ``,
+		expStatus: http.StatusOK,
+		expBody:   `[{"auth_mechanism":"basic","category":"helm","service":"helm"},{"auth_mechanism":"gcp","category":"helm","service":"gcs"},{"auth_mechanism":"aws","category":"helm","service":"s3"}]`,
+		useCookie: true,
+		validators: []func(c *publicIntTest, tester *tester, t *testing.T){
+			publicIntBodyValidator,
+		},
+	},
+}
+
+func TestHandleListHelmRepoIntegrations(t *testing.T) {
+	testPublicIntegrationRequests(t, listHelmRepoIntegrationsTest, true)
+}
+
 var listRepoIntegrationsTests = []*publicIntTest{
 	&publicIntTest{
 		initializers: []func(t *tester){
@@ -186,13 +209,43 @@ func TestHandleCreateAWSIntegration(t *testing.T) {
 	testPublicIntegrationRequests(t, createGCPIntegrationTests, true)
 }
 
+var createBasicIntegrationTests = []*publicIntTest{
+	&publicIntTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+		},
+		msg:      "Create basic integration",
+		method:   "POST",
+		endpoint: "/api/projects/1/integrations/basic",
+		body: `{
+			"username": "username",
+			"password": "password"
+		}`,
+		expStatus: http.StatusCreated,
+		expBody:   `{"id":1,"user_id":1,"project_id":1}`,
+		useCookie: true,
+		validators: []func(c *publicIntTest, tester *tester, t *testing.T){
+			basicIntBodyValidator,
+		},
+	},
+}
+
+func TestHandleCreateBasicIntegration(t *testing.T) {
+	testPublicIntegrationRequests(t, createBasicIntegrationTests, true)
+}
+
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 
 func publicIntBodyValidator(c *publicIntTest, tester *tester, t *testing.T) {
 	gotBody := make([]*ints.PorterIntegration, 0)
 	expBody := make([]*ints.PorterIntegration, 0)
 
-	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
+	bytes := tester.rr.Body.Bytes()
+
+	fmt.Println(string(bytes))
+
+	json.Unmarshal(bytes, &gotBody)
 	json.Unmarshal([]byte(c.expBody), &expBody)
 
 	if diff := deep.Equal(gotBody, expBody); diff != nil {
@@ -226,3 +279,20 @@ func awsIntBodyValidator(c *publicIntTest, tester *tester, t *testing.T) {
 		t.Error(diff)
 	}
 }
+
+func basicIntBodyValidator(c *publicIntTest, tester *tester, t *testing.T) {
+	gotBody := &ints.BasicIntegration{}
+	expBody := &ints.BasicIntegration{}
+
+	bytes := tester.rr.Body.Bytes()
+
+	fmt.Println(string(bytes))
+
+	json.Unmarshal(bytes, &gotBody)
+	json.Unmarshal([]byte(c.expBody), &expBody)
+
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
+	}
+}

+ 4 - 17
server/api/template_handler.go

@@ -19,27 +19,14 @@ import (
 // TODO: test and reduce fragility (handle untar/parse error for individual charts)
 // TODO: separate markdown retrieval into its own query if necessary
 func (app *App) HandleListTemplates(w http.ResponseWriter, r *http.Request) {
-	// repoIndex, err := loader.LoadRepoIndex("https://porter-dev.github.io/chart-repo/index.yaml")
-	repoIndex, err := loader.LoadRepoIndex("http://chartmuseum:8080/index.yaml")
+	repoIndex, err := loader.LoadRepoIndexPublic(app.ServerConf.HelmRepoURL)
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return
 	}
 
-	// Loop over charts in index.yaml
-	porterCharts := []models.PorterChartList{}
-
-	for _, entry := range repoIndex.Entries {
-		indexChart := entry[0]
-
-		porterChart := models.PorterChartList{}
-		porterChart.Name = indexChart.Name
-		porterChart.Description = indexChart.Description
-		porterChart.Icon = indexChart.Icon
-
-		porterCharts = append(porterCharts, porterChart)
-	}
+	porterCharts := loader.RepoIndexToPorterChartList(repoIndex)
 
 	json.NewEncoder(w).Encode(porterCharts)
 }
@@ -57,7 +44,7 @@ func (app *App) HandleReadTemplate(w http.ResponseWriter, r *http.Request) {
 	form := &forms.ChartForm{
 		Name:    name,
 		Version: version,
-		RepoURL: RepoURL: app.ServerConf.HelmRepoURL,
+		RepoURL: app.ServerConf.HelmRepoURL,
 	}
 
 	// if a repo_url is passed as query param, it will be populated
@@ -70,7 +57,7 @@ func (app *App) HandleReadTemplate(w http.ResponseWriter, r *http.Request) {
 
 	form.PopulateRepoURLFromQueryParams(vals)
 
-	chart, err := loader.LoadChart(form.RepoURL, form.Name, form.Version)
+	chart, err := loader.LoadChartPublic(form.RepoURL, form.Name, form.Version)
 
 	if err != nil {
 		fmt.Println("ERROR LOADING CHART", form.RepoURL, form.Name, form.Version, err)

+ 49 - 0
server/router/router.go

@@ -104,6 +104,14 @@ func New(
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/integrations/helm",
+			auth.BasicAuthenticate(
+				requestlog.NewHandler(a.HandleListHelmRepoIntegrations, l),
+			),
+		)
+
 		r.Method(
 			"GET",
 			"/integrations/repo",
@@ -290,6 +298,47 @@ func New(
 			),
 		)
 
+		r.Method(
+			"POST",
+			"/projects/{project_id}/integrations/basic",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleCreateBasicAuthIntegration, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
+		// /api/projects/{project_id}/helmrepos routes
+		r.Method(
+			"POST",
+			"/projects/{project_id}/helmrepos",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleCreateHelmRepo, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
+		r.Method(
+			"GET",
+			"/projects/{project_id}/helmrepos",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleListProjectHelmRepos, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
+		r.Method(
+			"GET",
+			"/projects/{project_id}/helmrepos/{helm_id}/charts",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleListHelmRepoCharts, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		// /api/projects/{project_id}/registries routes
 		r.Method(
 			"POST",