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

Merge branch 'beta.3.provisioning-integration' of https://github.com/porter-dev/porter into beta.3.integration-frontend

jusrhee 5 лет назад
Родитель
Сommit
9a7aac9cbe

+ 23 - 0
.github/workflows/gcr.yaml

@@ -0,0 +1,23 @@
+name: Build, Push to GCR.
+on:
+  push:
+    branches:
+      - staging
+jobs:
+  login-build-push:
+  runs-on: ubuntu-latest
+  steps:
+  - name: Set up Cloud SDK
+    uses: google-github-actions/setup-gcloud@master
+    with:
+      project_id: ${{ secrets.GCP_PROJECT_ID }}
+      service_account_key: ${{ secrets.GCP_SA_KEY }}
+      export_default_credentials: true
+  - name: Log in to gcloud CLI
+    run: gcloud auth configure-docker
+  - name: Checkout
+    uses: actions/checkout@v2.3.4
+  - name: Build
+    run: |
+      docker build . -t gcr.io/porter-dev-273614/porter-prov:latest -f ./docker/Dockerfile
+      docker push gcr.io/porter-dev-273614/porter-prov:latest

+ 37 - 0
cli/cmd/api/registry.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
 	"strings"
 	"strings"
+	"time"
 
 
 	"github.com/porter-dev/porter/internal/registry"
 	"github.com/porter-dev/porter/internal/registry"
 
 
@@ -165,6 +166,42 @@ func (c *Client) DeleteProjectRegistry(
 	return nil
 	return nil
 }
 }
 
 
+// GetECRTokenResponse blah
+type GetECRTokenResponse struct {
+	Token     string     `json:"token"`
+	ExpiresAt *time.Time `json:"expires_at"`
+}
+
+// GetECRAuthorizationToken gets an ECR authorization token
+func (c *Client) GetECRAuthorizationToken(
+	ctx context.Context,
+	projectID uint,
+	region string,
+) (*GetECRTokenResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/registries/ecr/%s/token", c.BaseURL, projectID, region),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	bodyResp := &GetECRTokenResponse{}
+	req = req.WithContext(ctx)
+
+	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
+}
+
 // ListRegistryRepositoryResponse is the list of repositories in a registry
 // ListRegistryRepositoryResponse is the list of repositories in a registry
 type ListRegistryRepositoryResponse []registry.Repository
 type ListRegistryRepositoryResponse []registry.Repository
 
 

+ 121 - 0
cli/cmd/docker.go

@@ -0,0 +1,121 @@
+package cmd
+
+import (
+	"context"
+	"encoding/json"
+	"io/ioutil"
+	"net/url"
+	"os"
+	"path/filepath"
+
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/github"
+	"github.com/spf13/cobra"
+
+	"github.com/docker/cli/cli/config/configfile"
+)
+
+var dockerCmd = &cobra.Command{
+	Use:   "docker",
+	Short: "Commands to configure Docker for a project",
+}
+
+var configureCmd = &cobra.Command{
+	Use:   "configure",
+	Short: "Configures the host's Docker instance",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, dockerConfig)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(dockerCmd)
+
+	dockerCmd.AddCommand(configureCmd)
+}
+
+func dockerConfig(user *api.AuthCheckResponse, client *api.Client, args []string) error {
+	pID := getProjectID()
+
+	// get all registries that should be added
+	regToAdd := make([]string, 0)
+
+	// get the list of namespaces
+	registries, err := client.ListRegistries(
+		context.Background(),
+		pID,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	for _, registry := range registries {
+		if registry.URL != "" {
+			// strip the protocol
+			regURL, err := url.Parse(registry.URL)
+
+			if err != nil {
+				continue
+			}
+
+			regToAdd = append(regToAdd, regURL.Host)
+		}
+	}
+
+	dockerConfigFile := filepath.Join(home, ".docker", "config.json")
+
+	// determine if configfile exists
+	if info, err := os.Stat(dockerConfigFile); info.IsDir() || os.IsNotExist(err) {
+		// if it does not exist, create it
+		err := ioutil.WriteFile(dockerConfigFile, []byte("{}"), 0700)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	// read the file bytes
+	configBytes, err := ioutil.ReadFile(dockerConfigFile)
+
+	if err != nil {
+		return err
+	}
+
+	// download the porter cred helper
+	z := &github.ZIPReleaseGetter{
+		AssetName:           "docker-credential-porter",
+		AssetFolderDest:     "/usr/local/bin",
+		ZipFolderDest:       filepath.Join(home, ".porter"),
+		ZipName:             "docker-credential-porter_latest.zip",
+		EntityID:            "porter-dev",
+		RepoName:            "porter",
+		IsPlatformDependent: true,
+	}
+
+	err = z.GetLatestRelease()
+
+	if err != nil {
+		return err
+	}
+
+	config := &configfile.ConfigFile{
+		Filename: dockerConfigFile,
+	}
+
+	err = json.Unmarshal(configBytes, config)
+
+	if err != nil {
+		return err
+	}
+
+	for _, regURL := range regToAdd {
+		config.CredentialHelpers[regURL] = "porter"
+	}
+
+	return config.Save()
+}

+ 2 - 0
cli/cmd/docker/porter.go

@@ -132,6 +132,8 @@ func StartPorter(opts *PorterStartOpts) (agent *Agent, id string, err error) {
 		}...)
 		}...)
 	}
 	}
 
 
+	opts.Env = append(opts.Env, "REDIS_ENABLED=false")
+
 	// create Porter container
 	// create Porter container
 	startOpts := PorterServerStartOpts{
 	startOpts := PorterServerStartOpts{
 		Name:          "porter_server_" + opts.ProcessID,
 		Name:          "porter_server_" + opts.ProcessID,

+ 144 - 46
cli/cmd/github/release.go

@@ -15,85 +15,183 @@ import (
 	"github.com/google/go-github/github"
 	"github.com/google/go-github/github"
 )
 )
 
 
-func getLatestReleaseDownloadURL() (string, string, error) {
-	client := github.NewClient(nil)
+// ZIPReleaseGetter retrieves a release from Github in ZIP format and downloads it
+// to a directory on host
+type ZIPReleaseGetter struct {
+	// The name of the asset, i.e. "porter", "portersvr", "static"
+	AssetName string
+
+	// The host folder destination of the asset
+	AssetFolderDest string
+
+	// The host folder destination for the .zip file
+	ZipFolderDest string
+
+	// The name of the .zip file to download to
+	ZipName string
+
+	// The name of the Github entity whose repo is queried: i.e. "porter-dev"
+	EntityID string
+
+	// The name of the Github repo to get releases from
+	RepoName string
+
+	// If the asset is platform dependent
+	IsPlatformDependent bool
+}
+
+// GetLatestRelease downloads the latest .zip release from a given Github repository
+func (z *ZIPReleaseGetter) GetLatestRelease() error {
+	releaseURL, err := z.getLatestReleaseDownloadURL()
+
+	if err != nil {
+		return err
+	}
+
+	return z.getReleaseFromURL(releaseURL)
+}
+
+// GetRelease downloads a specific .zip release from a given Github repository
+func (z *ZIPReleaseGetter) GetRelease(releaseTag string) error {
+	releaseURL, err := z.getReleaseDownloadURL(releaseTag)
+
+	fmt.Printf("getting release %s\n", releaseURL)
 
 
-	rel, _, err := client.Repositories.GetLatestRelease(context.Background(), "porter-dev", "porter")
+	if err != nil {
+		return err
+	}
+
+	return z.getReleaseFromURL(releaseURL)
+}
+
+func (z *ZIPReleaseGetter) getReleaseFromURL(releaseURL string) error {
+	fmt.Printf("getting release %s\n", releaseURL)
+
+	err := z.downloadToFile(releaseURL)
+
+	fmt.Printf("downloaded release %s to file %s\n", z.AssetName, filepath.Join(z.ZipFolderDest, z.ZipName))
 
 
 	if err != nil {
 	if err != nil {
-		return "", "", err
+		return err
 	}
 	}
 
 
-	var re *regexp.Regexp
+	fmt.Printf("unzipping %s to %s\n", z.AssetName, z.AssetFolderDest)
 
 
-	switch os := runtime.GOOS; os {
-	case "darwin":
-		re = regexp.MustCompile(`portersvr_.*_Darwin_x86_64\.zip`)
-	case "linux":
-		re = regexp.MustCompile(`portersvr_.*_Linux_x86_64\.zip`)
-	default:
-		fmt.Printf("%s.\n", os)
+	err = z.unzipToDir()
+
+	return err
+}
+
+// retrieves the download url for the latest release of an asset
+func (z *ZIPReleaseGetter) getLatestReleaseDownloadURL() (string, error) {
+	client := github.NewClient(nil)
+
+	rel, _, err := client.Repositories.GetLatestRelease(context.Background(), z.EntityID, z.RepoName)
+
+	if err != nil {
+		return "", err
 	}
 	}
 
 
-	staticRE := regexp.MustCompile(`static_.*\.zip`)
+	re, err := z.getDownloadRegexp()
+
+	if err != nil {
+		return "", err
+	}
 
 
 	releaseURL := ""
 	releaseURL := ""
-	staticReleaseURL := ""
 
 
 	// iterate through the assets
 	// iterate through the assets
 	for _, asset := range rel.Assets {
 	for _, asset := range rel.Assets {
 		if downloadURL := asset.GetBrowserDownloadURL(); re.MatchString(downloadURL) {
 		if downloadURL := asset.GetBrowserDownloadURL(); re.MatchString(downloadURL) {
 			releaseURL = downloadURL
 			releaseURL = downloadURL
-		} else if staticRE.MatchString(downloadURL) {
-			staticReleaseURL = downloadURL
 		}
 		}
 	}
 	}
 
 
-	return releaseURL, staticReleaseURL, nil
+	return releaseURL, nil
 }
 }
 
 
-// DownloadLatestServerRelease retrieves the latest Porter server release from Github, unzips
-// it, and adds the binary to the porter directory
-func DownloadLatestServerRelease(porterDir string) error {
-	releaseURL, staticReleaseURL, err := getLatestReleaseDownloadURL()
-	fmt.Println(releaseURL)
+func (z *ZIPReleaseGetter) getReleaseDownloadURL(releaseTag string) (string, error) {
+	client := github.NewClient(nil)
+
+	rel, _, err := client.Repositories.GetReleaseByTag(context.Background(), z.EntityID, z.RepoName, releaseTag)
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return "", fmt.Errorf("release %s does not exist", releaseTag)
 	}
 	}
 
 
-	zipFile := filepath.Join(porterDir, "portersrv_latest.zip")
-
-	err = downloadToFile(releaseURL, zipFile)
+	re, err := z.getDownloadRegexp()
 
 
 	if err != nil {
 	if err != nil {
-		return err
+		return "", err
 	}
 	}
 
 
-	err = unzipToDir(zipFile, porterDir)
+	releaseURL := ""
 
 
-	if err != nil {
-		return err
+	// iterate through the assets
+	for _, asset := range rel.Assets {
+		if downloadURL := asset.GetBrowserDownloadURL(); re.MatchString(downloadURL) {
+			releaseURL = downloadURL
+		}
 	}
 	}
 
 
-	staticZipFile := filepath.Join(porterDir, "static_latest.zip")
-
-	err = downloadToFile(staticReleaseURL, staticZipFile)
+	return releaseURL, nil
+}
 
 
-	if err != nil {
-		return err
+func (z *ZIPReleaseGetter) getDownloadRegexp() (*regexp.Regexp, error) {
+	if z.IsPlatformDependent {
+		switch os := runtime.GOOS; os {
+		case "darwin":
+			return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*_Darwin_x86_64\.zip`, z.AssetName)), nil
+		case "linux":
+			return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*_Linux_x86_64\.zip`, z.AssetName)), nil
+		default:
+			return nil, fmt.Errorf("%s is not a supported platform for Porter binaries", os)
+		}
 	}
 	}
 
 
-	staticDir := filepath.Join(porterDir, "static")
+	return regexp.MustCompile(fmt.Sprintf(`(?i)%s_.*\.zip`, z.AssetName)), nil
+}
 
 
-	err = unzipToDir(staticZipFile, staticDir)
+// // DownloadLatestServerRelease retrieves the latest Porter server release from Github, unzips
+// // it, and adds the binary to the porter directory
+// func DownloadLatestServerRelease(porterDir string) error {
+// 	releaseURL, staticReleaseURL, err := getLatestReleaseDownloadURL()
+// 	fmt.Println(releaseURL)
 
 
-	return err
-}
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	zipFile := filepath.Join(porterDir, "portersrv_latest.zip")
+
+// 	err = downloadToFile(releaseURL, zipFile)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	err = unzipToDir(zipFile, porterDir)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	staticZipFile := filepath.Join(porterDir, "static_latest.zip")
+
+// 	err = downloadToFile(staticReleaseURL, staticZipFile)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	staticDir := filepath.Join(porterDir, "static")
+
+// 	err = unzipToDir(staticZipFile, staticDir)
 
 
-func downloadToFile(url string, filepath string) error {
-	fmt.Println("Downloading:", url)
+// 	return err
+// }
 
 
+func (z *ZIPReleaseGetter) downloadToFile(url string) error {
 	// Get the data
 	// Get the data
 	resp, err := http.Get(url)
 	resp, err := http.Get(url)
 
 
@@ -104,7 +202,7 @@ func downloadToFile(url string, filepath string) error {
 	defer resp.Body.Close()
 	defer resp.Body.Close()
 
 
 	// Create the file
 	// Create the file
-	out, err := os.Create(filepath)
+	out, err := os.Create(filepath.Join(z.ZipFolderDest, z.ZipName))
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -118,8 +216,8 @@ func downloadToFile(url string, filepath string) error {
 	return err
 	return err
 }
 }
 
 
-func unzipToDir(zipfile string, dir string) error {
-	r, err := zip.OpenReader(zipfile)
+func (z *ZIPReleaseGetter) unzipToDir() error {
+	r, err := zip.OpenReader(filepath.Join(z.ZipFolderDest, z.ZipName))
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -129,10 +227,10 @@ func unzipToDir(zipfile string, dir string) error {
 
 
 	for _, f := range r.File {
 	for _, f := range r.File {
 		// Store filename/path for returning and using later on
 		// Store filename/path for returning and using later on
-		fpath := filepath.Join(dir, f.Name)
+		fpath := filepath.Join(z.AssetFolderDest, f.Name)
 
 
 		// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
 		// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
-		if !strings.HasPrefix(fpath, filepath.Clean(dir)+string(os.PathSeparator)) {
+		if !strings.HasPrefix(fpath, filepath.Clean(z.AssetFolderDest)+string(os.PathSeparator)) {
 			return fmt.Errorf("%s: illegal file path", fpath)
 			return fmt.Errorf("%s: illegal file path", fpath)
 		}
 		}
 
 

+ 3 - 3
cli/cmd/registry.go

@@ -120,15 +120,15 @@ func listRegistries(user *api.AuthCheckResponse, client *api.Client, args []stri
 	w := new(tabwriter.Writer)
 	w := new(tabwriter.Writer)
 	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
 	w.Init(os.Stdout, 3, 8, 0, '\t', tabwriter.AlignRight)
 
 
-	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "NAME", "SERVICE")
+	fmt.Fprintf(w, "%s\t%s\t%s\n", "ID", "URL", "SERVICE")
 
 
 	currRegistryID := getRegistryID()
 	currRegistryID := getRegistryID()
 
 
 	for _, registry := range registries {
 	for _, registry := range registries {
 		if currRegistryID == registry.ID {
 		if currRegistryID == registry.ID {
-			color.New(color.FgGreen).Fprintf(w, "%d\t%s\t%s (current registry)\n", registry.ID, registry.Name, registry.Service)
+			color.New(color.FgGreen).Fprintf(w, "%d\t%s\t%s (current registry)\n", registry.ID, registry.URL, registry.Service)
 		} else {
 		} else {
-			fmt.Fprintf(w, "%d\t%s\t%s\n", registry.ID, registry.Name, registry.Service)
+			fmt.Fprintf(w, "%d\t%s\t%s\n", registry.ID, registry.URL, registry.Service)
 		}
 		}
 	}
 	}
 
 

+ 9 - 5
cli/cmd/root.go

@@ -23,6 +23,15 @@ var home = homedir.HomeDir()
 // Execute adds all child commands to the root command and sets flags appropriately.
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 func Execute() {
 func Execute() {
+	Setup()
+
+	if err := rootCmd.Execute(); err != nil {
+		color.New(color.FgRed).Println(err)
+		os.Exit(1)
+	}
+}
+
+func Setup() {
 	// check that the .porter folder exists; create if not
 	// check that the .porter folder exists; create if not
 	porterDir := filepath.Join(home, ".porter")
 	porterDir := filepath.Join(home, ".porter")
 
 
@@ -54,9 +63,4 @@ func Execute() {
 			os.Exit(1)
 			os.Exit(1)
 		}
 		}
 	}
 	}
-
-	if err := rootCmd.Execute(); err != nil {
-		color.New(color.FgRed).Println(err)
-		os.Exit(1)
-	}
 }
 }

+ 31 - 1
cli/cmd/server.go

@@ -175,7 +175,7 @@ func startLocal(
 	staticFilePath := filepath.Join(home, ".porter", "static")
 	staticFilePath := filepath.Join(home, ".porter", "static")
 
 
 	if _, err := os.Stat(cmdPath); os.IsNotExist(err) {
 	if _, err := os.Stat(cmdPath); os.IsNotExist(err) {
-		err := github.DownloadLatestServerRelease(porterDir)
+		err := downloadLatestReleases(porterDir)
 
 
 		if err != nil {
 		if err != nil {
 			color.New(color.FgRed).Println("Failed:", err.Error())
 			color.New(color.FgRed).Println("Failed:", err.Error())
@@ -224,3 +224,33 @@ func stopDocker() error {
 
 
 	return nil
 	return nil
 }
 }
+
+func downloadLatestReleases(porterDir string) error {
+	z := &github.ZIPReleaseGetter{
+		AssetName:           "portersvr",
+		AssetFolderDest:     porterDir,
+		ZipFolderDest:       porterDir,
+		ZipName:             "portersvr_latest.zip",
+		EntityID:            "porter-dev",
+		RepoName:            "porter",
+		IsPlatformDependent: true,
+	}
+
+	err := z.GetLatestRelease()
+
+	if err != nil {
+		return err
+	}
+
+	zStatic := &github.ZIPReleaseGetter{
+		AssetName:           "static",
+		AssetFolderDest:     filepath.Join(porterDir, "static"),
+		ZipFolderDest:       porterDir,
+		ZipName:             "static_latest.zip",
+		EntityID:            "porter-dev",
+		RepoName:            "porter",
+		IsPlatformDependent: false,
+	}
+
+	return zStatic.GetLatestRelease()
+}

+ 15 - 12
cmd/app/main.go

@@ -30,14 +30,6 @@ func main() {
 		return
 		return
 	}
 	}
 
 
-	redis, err := adapter.NewRedisClient(&appConf.Redis)
-	prov.InitGlobalStream(redis)
-
-	if err != nil {
-		logger.Fatal().Err(err).Msg("")
-		return
-	}
-
 	err = db.AutoMigrate(
 	err = db.AutoMigrate(
 		&models.Project{},
 		&models.Project{},
 		&models.Role{},
 		&models.Role{},
@@ -75,6 +67,21 @@ func main() {
 
 
 	repo := gorm.NewRepository(db, &key)
 	repo := gorm.NewRepository(db, &key)
 
 
+	if appConf.Redis.Enabled {
+		redis, err := adapter.NewRedisClient(&appConf.Redis)
+
+		if err != nil {
+			logger.Fatal().Err(err).Msg("")
+			return
+		}
+
+		prov.InitGlobalStream(redis)
+
+		errorChan := make(chan error)
+
+		go prov.GlobalStreamListener(redis, *repo, errorChan)
+	}
+
 	a, _ := api.New(&api.AppConfig{
 	a, _ := api.New(&api.AppConfig{
 		Logger:     logger,
 		Logger:     logger,
 		Repository: repo,
 		Repository: repo,
@@ -96,10 +103,6 @@ func main() {
 		IdleTimeout:  appConf.Server.TimeoutIdle,
 		IdleTimeout:  appConf.Server.TimeoutIdle,
 	}
 	}
 
 
-	errorChan := make(chan error)
-
-	go prov.GlobalStreamListener(redis, *repo, errorChan)
-
 	if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
 	if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
 		log.Fatal("Server startup failed", err)
 		log.Fatal("Server startup failed", err)
 	}
 	}

+ 196 - 0
cmd/docker-credential-porter/helper/cache.go

@@ -0,0 +1,196 @@
+package helper
+
+import (
+	"crypto/md5"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"k8s.io/client-go/util/homedir"
+)
+
+type CredentialsCache interface {
+	Get(registry string) *AuthEntry
+	Set(registry string, entry *AuthEntry)
+	List() []*AuthEntry
+	Clear()
+}
+
+type AuthEntry struct {
+	AuthorizationToken string
+	RequestedAt        time.Time
+	ExpiresAt          time.Time
+	ProxyEndpoint      string
+}
+
+// IsValid checks if AuthEntry is still valid at testTime. AuthEntries expire at 1/2 of their original
+// requested window.
+func (authEntry *AuthEntry) IsValid(testTime time.Time) bool {
+	validWindow := authEntry.ExpiresAt.Sub(authEntry.RequestedAt)
+	refreshTime := authEntry.ExpiresAt.Add(-1 * validWindow / time.Duration(2))
+	return testTime.Before(refreshTime)
+}
+
+func BuildCredentialsCache(region string) CredentialsCache {
+	home := homedir.HomeDir()
+	cacheDir := filepath.Join(home, ".porter")
+	cacheFilename := "cache.json"
+
+	return NewFileCredentialsCache(cacheDir, cacheFilename, region)
+}
+
+// Determine a key prefix for a credentials cache. Because auth tokens are scoped to an account and region, rely on provided
+// region, as well as hash of the access key.
+func credentialsCachePrefix(region string, credentials *credentials.Value) string {
+	return fmt.Sprintf("%s-%s-", region, checksum(credentials.AccessKeyID))
+}
+
+// Base64 encodes an MD5 checksum. Relied on for uniqueness, and not for cryptographic security.
+func checksum(text string) string {
+	hasher := md5.New()
+	data := hasher.Sum([]byte(text))
+	return base64.StdEncoding.EncodeToString(data)
+}
+
+const registryCacheVersion = "1.0"
+
+type RegistryCache struct {
+	Registries map[string]*AuthEntry
+	Version    string
+}
+
+type fileCredentialCache struct {
+	path           string
+	filename       string
+	cachePrefixKey string
+}
+
+func newRegistryCache() *RegistryCache {
+	return &RegistryCache{
+		Registries: make(map[string]*AuthEntry),
+		Version:    registryCacheVersion,
+	}
+}
+
+// NewFileCredentialsCache returns a new file credentials cache.
+//
+// path is used for temporary files during save, and filename should be a relative filename
+// in the same directory where the cache is serialized and deserialized.
+//
+// cachePrefixKey is used for scoping credentials for a given credential cache (i.e. region and
+// accessKey).
+func NewFileCredentialsCache(path string, filename string, cachePrefixKey string) CredentialsCache {
+	if _, err := os.Stat(path); err != nil {
+		os.MkdirAll(path, 0700)
+	}
+
+	return &fileCredentialCache{path: path, filename: filename, cachePrefixKey: cachePrefixKey}
+}
+
+func (f *fileCredentialCache) Get(registry string) *AuthEntry {
+	registryCache := f.init()
+
+	return registryCache.Registries[f.cachePrefixKey+registry]
+}
+
+func (f *fileCredentialCache) Set(registry string, entry *AuthEntry) {
+	registryCache := f.init()
+
+	registryCache.Registries[f.cachePrefixKey+registry] = entry
+
+	f.save(registryCache)
+}
+
+// List returns all of the available AuthEntries (regardless of prefix)
+func (f *fileCredentialCache) List() []*AuthEntry {
+	registryCache := f.init()
+
+	// optimize allocation for copy
+	entries := make([]*AuthEntry, 0, len(registryCache.Registries))
+
+	for _, entry := range registryCache.Registries {
+		entries = append(entries, entry)
+	}
+
+	return entries
+}
+
+func (f *fileCredentialCache) Clear() {
+	os.Remove(f.fullFilePath())
+}
+
+func (f *fileCredentialCache) fullFilePath() string {
+	return filepath.Join(f.path, f.filename)
+}
+
+// Saves credential cache to disk. This writes to a temporary file first, then moves the file to the config location.
+// This eliminates from reading partially written credential files, and reduces (but does not eliminate) concurrent
+// file access. There is not guarantee here for handling multiple writes at once since there is no out of process locking.
+func (f *fileCredentialCache) save(registryCache *RegistryCache) error {
+	file, err := ioutil.TempFile(f.path, ".config.json.tmp")
+	if err != nil {
+		return err
+	}
+
+	buff, err := json.MarshalIndent(registryCache, "", "  ")
+	if err != nil {
+		file.Close()
+		os.Remove(file.Name())
+		return err
+	}
+
+	_, err = file.Write(buff)
+
+	if err != nil {
+		file.Close()
+		os.Remove(file.Name())
+		return err
+	}
+
+	file.Close()
+	// note this is only atomic when relying on linux syscalls
+	os.Rename(file.Name(), f.fullFilePath())
+	return err
+}
+
+func (f *fileCredentialCache) init() *RegistryCache {
+	registryCache, err := f.load()
+	if err != nil {
+		f.Clear()
+		registryCache = newRegistryCache()
+	}
+	return registryCache
+}
+
+// Loading a cache from disk will return errors for malformed or incompatible cache files.
+func (f *fileCredentialCache) load() (*RegistryCache, error) {
+	registryCache := newRegistryCache()
+
+	file, err := os.Open(f.fullFilePath())
+	if os.IsNotExist(err) {
+		return registryCache, nil
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	defer file.Close()
+
+	if err = json.NewDecoder(file).Decode(&registryCache); err != nil {
+		return nil, err
+	}
+
+	if registryCache.Version != registryCacheVersion {
+		return nil, fmt.Errorf("ecr: Registry cache version %#v is not compatible with %#v, ignoring existing cache",
+			registryCache.Version,
+			registryCacheVersion)
+	}
+
+	return registryCache, nil
+}

+ 150 - 0
cmd/docker-credential-porter/helper/helper.go

@@ -0,0 +1,150 @@
+package helper
+
+import (
+	"context"
+	"encoding/base64"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/docker/docker-credential-helpers/credentials"
+	"github.com/porter-dev/porter/cli/cmd"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/spf13/viper"
+	"k8s.io/client-go/util/homedir"
+)
+
+// PorterHelper implements credentials.Helper: it acts as a credentials
+// helper for Docker that allows authentication with different registries.
+type PorterHelper struct {
+	Debug bool
+
+	credCache CredentialsCache
+}
+
+// Add appends credentials to the store.
+func (p *PorterHelper) Add(cr *credentials.Credentials) error {
+	// Doesn't seem to be called
+	return nil
+}
+
+// Delete removes credentials from the store.
+func (p *PorterHelper) Delete(serverURL string) error {
+	// Doesn't seem to be called
+	return nil
+}
+
+var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?`)
+
+// Get retrieves credentials from the store.
+// It returns username and secret as strings.
+func (p *PorterHelper) Get(serverURL string) (user string, secret string, err error) {
+	// parse the server url for region
+	matches := ecrPattern.FindStringSubmatch(serverURL)
+
+	if len(matches) == 0 {
+		err := fmt.Errorf("only ECR registry URLs are supported")
+
+		if p.Debug {
+			log.Printf("Error: %s\n", err.Error())
+		}
+
+		return "", "", err
+	} else if len(matches) < 3 {
+		err := fmt.Errorf("%s is not a valid ECR repository URI", serverURL)
+
+		if p.Debug {
+			log.Printf("Error: %s\n", err.Error())
+		}
+
+		return "", "", err
+	}
+
+	region := matches[3]
+
+	credCache := BuildCredentialsCache(region)
+	cachedEntry := credCache.Get(serverURL)
+
+	var token string
+
+	if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
+		token = cachedEntry.AuthorizationToken
+	} else {
+		host := viper.GetString("host")
+		projID := viper.GetUint("project")
+
+		client := api.NewClient(host+"/api", "cookie.json")
+
+		// get a token from the server
+		tokenResp, err := client.GetECRAuthorizationToken(context.Background(), projID, matches[3])
+
+		if err != nil {
+			return "", "", err
+		}
+
+		token = tokenResp.Token
+
+		// set the token in cache
+		credCache.Set(serverURL, &AuthEntry{
+			AuthorizationToken: token,
+			RequestedAt:        time.Now(),
+			ExpiresAt:          *tokenResp.ExpiresAt,
+			ProxyEndpoint:      serverURL,
+		})
+	}
+
+	return p.getAuth(token)
+}
+
+// List returns the stored serverURLs and their associated usernames.
+func (p *PorterHelper) List() (map[string]string, error) {
+	credCache := BuildCredentialsCache("")
+	entries := credCache.List()
+
+	res := make(map[string]string)
+
+	for _, entry := range entries {
+		user, _, err := p.getAuth(entry.AuthorizationToken)
+
+		if err != nil {
+			continue
+		}
+
+		res[entry.ProxyEndpoint] = user
+	}
+
+	return res, nil
+}
+
+func (p *PorterHelper) getAuth(token string) (string, string, error) {
+	decodedToken, err := base64.StdEncoding.DecodeString(token)
+
+	if err != nil {
+		return "", "", fmt.Errorf("Invalid token: %v", err)
+	}
+
+	parts := strings.SplitN(string(decodedToken), ":", 2)
+
+	if len(parts) < 2 {
+		return "", "", fmt.Errorf("Invalid token: expected two parts, got %d", len(parts))
+	}
+
+	return parts[0], parts[1], nil
+}
+
+func (p *PorterHelper) init() {
+	cmd.Setup()
+
+	if p.Debug {
+		var home = homedir.HomeDir()
+		file, err := os.OpenFile(filepath.Join(home, ".porter", "logs.txt"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
+
+		if err == nil {
+			log.SetOutput(file)
+		}
+	}
+}

+ 29 - 0
cmd/docker-credential-porter/main.go

@@ -0,0 +1,29 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"github.com/docker/docker-credential-helpers/credentials"
+	"github.com/porter-dev/porter/cmd/docker-credential-porter/helper"
+)
+
+// Version will be linked by an ldflag during build
+var Version string = "dev"
+
+func main() {
+	var versionFlag bool
+	flag.BoolVar(&versionFlag, "version", false, "print version and exit")
+	flag.Parse()
+
+	// Exit safely when version is used
+	if versionFlag {
+		fmt.Println(Version)
+		os.Exit(0)
+	}
+
+	credentials.Serve(&helper.PorterHelper{
+		Debug: Version == "dev",
+	})
+}

+ 60 - 27
dashboard/src/main/home/new-project/NewProject.tsx

@@ -193,9 +193,66 @@ export default class NewProject extends Component<PropsType, StateType> {
     return false;
     return false;
   }
   }
 
 
+  provisionECR = (proj: ProjectType, callback: (proj: ProjectType, ecr: any) => void) => {
+    let { awsAccessId, awsSecretKey, awsRegion } = this.state;
+
+    api.createAWSIntegration('<token>', {
+      aws_region: awsRegion,
+      aws_access_key_id: awsAccessId,
+      aws_secret_access_key: awsSecretKey,
+    }, { id: proj.id }, (err: any, res: any) => {
+      if (err) {
+        console.log(err);
+        return;
+      }
+
+      api.provisionECR('<token>', {
+        aws_integration_id: res.data.id,
+        ecr_name: `${proj.name}-registry`
+      }, {id: proj.id}, (err: any, ecr:any) => {
+        if (err) {
+          console.log(err)
+          return;
+        }
+
+        callback(proj, ecr);
+      })
+      
+    });
+  }
+
+  provisionEKS = (proj: ProjectType, ecr: any) => {
+    let { awsAccessId, awsSecretKey, awsRegion } = this.state;
+
+    api.createAWSIntegration('<token>', {
+      aws_region: awsRegion,
+      aws_access_key_id: awsAccessId,
+      aws_secret_access_key: awsSecretKey,
+    }, { id: proj.id }, (err: any, res: any) => {
+      if (err) {
+        console.log(err)
+        return;
+      }
+
+      api.provisionEKS('<token>', {
+        aws_integration_id: res.data.id,
+        eks_name: `${proj.name}-cluster`,
+      }, { id: proj.id}, (err: any, eks: any) => {
+        if (err) {
+          console.log(err)
+          return;
+        }
+
+        this.props.setCurrentView('provisioner', [
+          {infra_id: ecr?.data?.id, kind: ecr?.data?.kind},
+          {infra_id: eks?.data?.id, kind: eks?.data?.kind},
+        ]);
+      })
+    })
+  }
+
   createProject = () => {
   createProject = () => {
     this.setState({ status: 'loading' });
     this.setState({ status: 'loading' });
-    let { awsAccessId, awsSecretKey, awsRegion } = this.state;
 
 
     api.createProject('<token>', {
     api.createProject('<token>', {
       name: this.state.projectName
       name: this.state.projectName
@@ -212,33 +269,9 @@ export default class NewProject extends Component<PropsType, StateType> {
             if (res.data.length > 0) {
             if (res.data.length > 0) {
               let proj = res.data.find((el: ProjectType) => el.name === this.state.projectName);
               let proj = res.data.find((el: ProjectType) => el.name === this.state.projectName);
               this.context.setCurrentProject(proj);
               this.context.setCurrentProject(proj);
-
-              // Handle provisioning logic
+              
               if (this.state.selectedProvider === 'aws') {
               if (this.state.selectedProvider === 'aws') {
-                let clusterName = `${proj.name}-cluster`
-
-                api.createAWSIntegration('<token>', {
-                  aws_region: awsRegion,
-                  aws_cluster_id: clusterName,
-                  aws_access_key_id: awsAccessId,
-                  aws_secret_access_key: awsSecretKey,
-                }, { id: proj.id }, (err2: any, res2: any) => {
-                  if (err2) {
-                    console.log(err2);
-                  } else {
-                    api.provisionEKS('<token>', {
-                      aws_integration_id: res2.data.id,
-                      eks_name: clusterName,
-                    }, {id: proj.id}, (err3: any, res3:any) => {
-                      if (err3) {
-                        console.log(err3)
-                      } else {
-                        this.props.setCurrentView('provisioner', { infra_id: res3.data.id, kind: res3.data.kind });
-                      }
-                    })
-                  }
-                });
-
+                this.provisionECR(proj, this.provisionEKS)
               } else {
               } else {
                 this.props.setCurrentView('dashboard', null);
                 this.props.setCurrentView('dashboard', null);
               }
               }

+ 66 - 22
dashboard/src/main/home/new-project/Provisioner.tsx

@@ -7,6 +7,7 @@ import { integrationList } from '../../../shared/common';
 import loading from '../../../assets/loading.gif';
 import loading from '../../../assets/loading.gif';
 
 
 import Helper from '../../../components/values-form/Helper';
 import Helper from '../../../components/values-form/Helper';
+import { eventNames } from 'process';
 
 
 type PropsType = {
 type PropsType = {
   viewData: any,
   viewData: any,
@@ -14,15 +15,17 @@ type PropsType = {
 
 
 type StateType = {
 type StateType = {
   logs: string[],
   logs: string[],
-  ws: any
+  websockets: any[],
+  maxStep : Record<string, number>,
+  currentStep: Record<string, number>,
 };
 };
 
 
-const loadMax = 40;
-
 export default class Provisioner extends Component<PropsType, StateType> {
 export default class Provisioner extends Component<PropsType, StateType> {
   state = {
   state = {
     logs: [] as string[],
     logs: [] as string[],
-    ws : null as any
+    websockets : [] as any[],
+    maxStep: {} as Record<string, any>,
+    currentStep: {} as Record<string, number>,
   }
   }
 
 
   scrollToBottom = () => {
   scrollToBottom = () => {
@@ -32,36 +35,66 @@ export default class Provisioner extends Component<PropsType, StateType> {
   componentDidMount() {
   componentDidMount() {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
     let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
     let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
-    let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${this.props.viewData.kind}/${this.props.viewData.infra_id}/logs`)
 
 
-    this.setState({ ws }, () => {
-      if (!this.state.ws) return;
-  
-      this.state.ws.onopen = () => {
+    let websockets = this.props.viewData.forEach((infra: any) => {
+      let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${infra.kind}/${infra.infra_id}/logs`)
+      
+      ws.onopen = () => {
         console.log('connected to websocket')
         console.log('connected to websocket')
       }
       }
-  
-      this.state.ws.onmessage = (evt: MessageEvent) => {
+
+      ws.onmessage = (evt: MessageEvent) => {
         let event = JSON.parse(evt.data)
         let event = JSON.parse(evt.data)
-        let data = event.map((msg: any) => { return msg["Values"]["data"]})
-        this.setState({ logs: [...this.state.logs, ...data] }, () => {
+        let data = event.map((msg: any) => { return `${infra.kind}: ${msg["Values"]["data"]}` })
+        let err = null
+
+        // check for error
+        event.forEach((e: any) => {
+          err = e["Values"]["kind"] == "error" ? e["Values"]["data"] : null
+        })
+
+        if (err) {
+          this.setState({ logs: [err] })
+        }
+        
+        if (!this.state.maxStep[infra.kind]) {
+          this.setState({
+            maxStep: {
+              ...this.state.maxStep,
+              [infra.kind] : event[event.length]["Values"]["created_resources"]
+            }
+          })
+        }
+
+        this.setState({ 
+          logs: [...this.state.logs, ...data], 
+          currentStep: {
+            ...this.state.currentStep,
+            [infra.kind] : event[event.length]["Values"]["created_resources"]
+          },
+        }, () => {
           this.scrollToBottom()
           this.scrollToBottom()
         })
         })
       }
       }
-  
-      this.state.ws.onerror = (err: ErrorEvent) => {
+
+      ws.onerror = (err: ErrorEvent) => {
         console.log(err)
         console.log(err)
       }
       }
-    })
 
 
-    this.setState({ logs: [] });
+      ws.onclose = () => {
+        console.log('closing provisioner websocket')
+      }
+
+      return ws
+    });
+
+    this.setState({ websockets, logs: [] });
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
-    if (this.state.ws) {
-      console.log('closing websocket')
-      this.state.ws.close()
-    }
+    this.state.websockets?.forEach((ws) => {
+      ws.close()
+    })
   }
   }
 
 
   scrollRef = React.createRef<HTMLDivElement>();
   scrollRef = React.createRef<HTMLDivElement>();
@@ -73,6 +106,17 @@ export default class Provisioner extends Component<PropsType, StateType> {
   }
   }
   
   
   render() {
   render() {
+    let maxStep = 0;
+    let currentStep = 0;
+
+    for (let key in this.state.maxStep) {
+      maxStep += this.state.maxStep[key]
+    }
+
+    for (let key in this.state.currentStep) {
+      currentStep += this.state.currentStep[key]
+    }
+
     return (
     return (
       <StyledProvisioner>
       <StyledProvisioner>
         <TitleSection>
         <TitleSection>
@@ -84,7 +128,7 @@ export default class Provisioner extends Component<PropsType, StateType> {
         </Helper>
         </Helper>
 
 
         <LoadingBar>
         <LoadingBar>
-          <Loaded progress={((7 / loadMax) * 100).toString() + '%'} />
+          <Loaded progress={((currentStep / maxStep) * 100).toString() + '%'} />
         </LoadingBar>
         </LoadingBar>
 
 
         <LogStream ref={this.scrollRef}>
         <LogStream ref={this.scrollRef}>

+ 5 - 2
go.mod

@@ -8,10 +8,13 @@ require (
 	github.com/Azure/go-autorest/autorest/adal v0.9.5 // indirect
 	github.com/Azure/go-autorest/autorest/adal v0.9.5 // indirect
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Masterminds/semver v1.5.0 // indirect
-	github.com/aws/aws-sdk-go v1.31.6
+	github.com/aws/aws-sdk-go v1.35.4
+	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20201113001948-d77edb6d2e47
 	github.com/containerd/containerd v1.4.1 // indirect
 	github.com/containerd/containerd v1.4.1 // indirect
 	github.com/coreos/rkt v1.30.0
 	github.com/coreos/rkt v1.30.0
+	github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492
 	github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce
 	github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce
+	github.com/docker/docker-credential-helpers v0.6.3
 	github.com/docker/go-connections v0.4.0
 	github.com/docker/go-connections v0.4.0
 	github.com/evanphx/json-patch v4.9.0+incompatible // indirect
 	github.com/evanphx/json-patch v4.9.0+incompatible // indirect
 	github.com/fatih/color v1.9.0
 	github.com/fatih/color v1.9.0
@@ -46,7 +49,7 @@ require (
 	github.com/pelletier/go-toml v1.8.1 // indirect
 	github.com/pelletier/go-toml v1.8.1 // indirect
 	github.com/pkg/errors v0.9.1
 	github.com/pkg/errors v0.9.1
 	github.com/rs/zerolog v1.20.0
 	github.com/rs/zerolog v1.20.0
-	github.com/sirupsen/logrus v1.7.0 // indirect
+	github.com/sirupsen/logrus v1.7.0
 	github.com/spf13/cobra v1.0.0
 	github.com/spf13/cobra v1.0.0
 	github.com/spf13/viper v1.4.0
 	github.com/spf13/viper v1.4.0
 	github.com/stretchr/testify v1.6.1
 	github.com/stretchr/testify v1.6.1

+ 10 - 0
go.sum

@@ -128,7 +128,12 @@ github.com/aws/aws-sdk-go v1.30.0 h1:7NDwnnQrI1Ivk0bXLzMmuX5ozzOwteHOsAs4druW7gI
 github.com/aws/aws-sdk-go v1.30.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
 github.com/aws/aws-sdk-go v1.30.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
 github.com/aws/aws-sdk-go v1.31.6 h1:nKjQbpXhdImctBh1e0iLg9iQW/X297LPPuY/9f92R2k=
 github.com/aws/aws-sdk-go v1.31.6 h1:nKjQbpXhdImctBh1e0iLg9iQW/X297LPPuY/9f92R2k=
 github.com/aws/aws-sdk-go v1.31.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
 github.com/aws/aws-sdk-go v1.31.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.35.4 h1:GG0sdhmzQSe4/UcF9iuQP9i+58bPRyU4OpujyzMlVjo=
+github.com/aws/aws-sdk-go v1.35.4/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
+github.com/awslabs/amazon-ecr-credential-helper v0.4.0 h1:LYTmunbYJ8piWElip5hW2NpkEW5JfCbeB9hVHn5LIrc=
+github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20201113001948-d77edb6d2e47 h1:iBW2usmd8V2GsiAWIGGR1YCmdFm/iseBZyhTyx/ro6Q=
+github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20201113001948-d77edb6d2e47/go.mod h1:Z4a0MOGfjhxYBJ5E6pcbKUNVJ0bDyhiz68+N76ZtKhE=
 github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -417,6 +422,7 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18h
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1-0.20190508161146-9fa652df1129/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
 github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
@@ -632,6 +638,9 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5i
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
 github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
 github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
 github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
 github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd h1:nIzoSW6OhhppWLm4yqBwZsKJlAayUu5FGozhrF3ETSM=
 github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd h1:nIzoSW6OhhppWLm4yqBwZsKJlAayUu5FGozhrF3ETSM=
@@ -1175,6 +1184,7 @@ golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190812172437-4e8604ab3aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190812172437-4e8604ab3aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

+ 3 - 0
internal/config/redis.go

@@ -2,6 +2,9 @@ package config
 
 
 // RedisConf is the redis config required for the provisioner container
 // RedisConf is the redis config required for the provisioner container
 type RedisConf struct {
 type RedisConf struct {
+	// if redis should be used
+	Enabled bool `env:"REDIS_ENABLED,default=true"`
+
 	Host     string `env:"REDIS_HOST,default=redis"`
 	Host     string `env:"REDIS_HOST,default=redis"`
 	Port     string `env:"REDIS_PORT,default=6379"`
 	Port     string `env:"REDIS_PORT,default=6379"`
 	Username string `env:"REDIS_USER"`
 	Username string `env:"REDIS_USER"`

+ 1 - 0
internal/forms/registry.go

@@ -10,6 +10,7 @@ import (
 type CreateRegistry struct {
 type CreateRegistry struct {
 	Name             string `json:"name" form:"required"`
 	Name             string `json:"name" form:"required"`
 	ProjectID        uint   `json:"project_id" form:"required"`
 	ProjectID        uint   `json:"project_id" form:"required"`
+	URL              string `json:"url"`
 	GCPIntegrationID uint   `json:"gcp_integration_id"`
 	GCPIntegrationID uint   `json:"gcp_integration_id"`
 	AWSIntegrationID uint   `json:"aws_integration_id"`
 	AWSIntegrationID uint   `json:"aws_integration_id"`
 }
 }

+ 7 - 0
internal/models/registry.go

@@ -13,6 +13,9 @@ type Registry struct {
 	// Name of the registry
 	// Name of the registry
 	Name string `json:"name"`
 	Name string `json:"name"`
 
 
+	// URL of the registry
+	URL string `json:"url"`
+
 	// The project that this integration belongs to
 	// The project that this integration belongs to
 	ProjectID uint `json:"project_id"`
 	ProjectID uint `json:"project_id"`
 
 
@@ -37,6 +40,9 @@ type RegistryExternal struct {
 	// Name of the registry
 	// Name of the registry
 	Name string `json:"name"`
 	Name string `json:"name"`
 
 
+	// URL of the registry
+	URL string `json:"url"`
+
 	// The integration service for this registry
 	// The integration service for this registry
 	Service integrations.IntegrationService `json:"service"`
 	Service integrations.IntegrationService `json:"service"`
 }
 }
@@ -55,6 +61,7 @@ func (r *Registry) Externalize() *RegistryExternal {
 		ID:        r.ID,
 		ID:        r.ID,
 		ProjectID: r.ProjectID,
 		ProjectID: r.ProjectID,
 		Name:      r.Name,
 		Name:      r.Name,
+		URL:       r.URL,
 		Service:   serv,
 		Service:   serv,
 	}
 	}
 }
 }

+ 104 - 0
server/api/registry_handler.go

@@ -4,12 +4,15 @@ import (
 	"encoding/json"
 	"encoding/json"
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
+	"time"
 
 
 	"github.com/porter-dev/porter/internal/registry"
 	"github.com/porter-dev/porter/internal/registry"
 
 
 	"github.com/go-chi/chi"
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
+
+	"github.com/aws/aws-sdk-go/service/ecr"
 )
 )
 
 
 // HandleCreateRegistry creates a new registry
 // HandleCreateRegistry creates a new registry
@@ -45,6 +48,34 @@ func (app *App) HandleCreateRegistry(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
+	// if the registry is ECR and URL is not set, get the registry url
+	if registry.URL == "" && registry.AWSIntegrationID != 0 {
+		awsInt, err := app.Repo.AWSIntegration.ReadAWSIntegration(registry.AWSIntegrationID)
+
+		if err != nil {
+			app.handleErrorDataRead(err, w)
+			return
+		}
+
+		sess, err := awsInt.GetSession()
+
+		if err != nil {
+			app.handleErrorDataRead(err, w)
+			return
+		}
+
+		ecrSvc := ecr.New(sess)
+
+		output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+
+		if err != nil {
+			app.handleErrorDataRead(err, w)
+			return
+		}
+
+		registry.URL = *output.AuthorizationData[0].ProxyEndpoint
+	}
+
 	// handle write to the database
 	// handle write to the database
 	registry, err = app.Repo.Registry.CreateRegistry(registry)
 	registry, err = app.Repo.Registry.CreateRegistry(registry)
 
 
@@ -95,6 +126,79 @@ func (app *App) HandleListProjectRegistries(w http.ResponseWriter, r *http.Reque
 	}
 	}
 }
 }
 
 
+// temp -- token response
+type ECRTokenResponse struct {
+	Token     string     `json:"token"`
+	ExpiresAt *time.Time `json:"expires_at"`
+}
+
+// HandleGetProjectRegistryECRToken gets an ECR token for a registry
+func (app *App) HandleGetProjectRegistryECRToken(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
+	}
+
+	region := chi.URLParam(r, "region")
+
+	if region == "" {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// list registries and find one that matches the region
+	regs, err := app.Repo.Registry.ListRegistriesByProjectID(uint(projID))
+	var token string
+	var expiresAt *time.Time
+
+	for _, reg := range regs {
+		if reg.AWSIntegrationID != 0 {
+			awsInt, err := app.Repo.AWSIntegration.ReadAWSIntegration(reg.AWSIntegrationID)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			if awsInt.AWSRegion == region {
+				// get the aws integration and session
+				sess, err := awsInt.GetSession()
+
+				if err != nil {
+					app.handleErrorDataRead(err, w)
+					return
+				}
+
+				ecrSvc := ecr.New(sess)
+
+				output, err := ecrSvc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
+
+				if err != nil {
+					app.handleErrorDataRead(err, w)
+					return
+				}
+
+				token = *output.AuthorizationData[0].AuthorizationToken
+				expiresAt = output.AuthorizationData[0].ExpiresAt
+			}
+		}
+	}
+
+	resp := &ECRTokenResponse{
+		Token:     token,
+		ExpiresAt: expiresAt,
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 // HandleUpdateProjectRegistry updates a registry
 // HandleUpdateProjectRegistry updates a registry
 func (app *App) HandleUpdateProjectRegistry(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleUpdateProjectRegistry(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)

+ 10 - 0
server/router/router.go

@@ -420,6 +420,16 @@ func New(a *api.App) *chi.Mux {
 			),
 			),
 		)
 		)
 
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/registries/ecr/{region}/token",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleGetProjectRegistryECRToken, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		r.Method(
 		r.Method(
 			"DELETE",
 			"DELETE",
 			"/projects/{project_id}/registries/{registry_id}",
 			"/projects/{project_id}/registries/{registry_id}",