Pārlūkot izejas kodu

merged redis conf

Alexander Belanger 5 gadi atpakaļ
vecāks
revīzija
b27df994a7

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

@@ -165,6 +165,41 @@ func (c *Client) DeleteProjectRegistry(
 	return nil
 }
 
+// GetECRTokenResponse blah
+type GetECRTokenResponse struct {
+	Token string `json:"token"`
+}
+
+// GetECRAuthorizationToken gets an ECR authorization token
+func (c *Client) GetECRAuthorizationToken(
+	ctx context.Context,
+	projectID uint,
+	registryID uint,
+) error {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/registries/%d/ecr/token", c.BaseURL, projectID, registryID),
+		nil,
+	)
+
+	if err != nil {
+		return err
+	}
+
+	bodyResp := &GetECRTokenResponse{}
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return err
+	}
+
+	return nil
+}
+
 // ListRegistryRepositoryResponse is the list of repositories in a registry
 type ListRegistryRepositoryResponse []registry.Repository
 

+ 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.
 // This is called by main.main(). It only needs to happen once to the rootCmd.
 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
 	porterDir := filepath.Join(home, ".porter")
 
@@ -54,9 +63,4 @@ func Execute() {
 			os.Exit(1)
 		}
 	}
-
-	if err := rootCmd.Execute(); err != nil {
-		color.New(color.FgRed).Println(err)
-		os.Exit(1)
-	}
 }

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

@@ -0,0 +1,67 @@
+package helper
+
+import (
+	"context"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+
+	"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{}
+
+// 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
+}
+
+// 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) {
+	cmd.Setup()
+	var home = homedir.HomeDir()
+	file, _ := os.OpenFile(filepath.Join(home, ".porter", "logs.txt"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
+	log.SetOutput(file)
+
+	host := viper.GetString("host")
+	projID := viper.GetUint("project")
+
+	client := api.NewClient(host+"/api", "cookie.json")
+
+	// list registries
+	reg, err := client.ListRegistries(context.Background(), projID)
+
+	log.Println("called regs", reg, err)
+
+	if err != nil {
+		return "", "", err
+	}
+
+	log.Println(reg)
+
+	return "", "", nil
+}
+
+// List returns the stored serverURLs and their associated usernames.
+func (p *PorterHelper) List() (map[string]string, error) {
+	var home = homedir.HomeDir()
+
+	ioutil.WriteFile(filepath.Join(home, ".porter", "log.txt"), []byte("called list\n"), 0644)
+
+	return nil, nil
+}

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

@@ -0,0 +1,10 @@
+package main
+
+import (
+	"github.com/docker/docker-credential-helpers/credentials"
+	"github.com/porter-dev/porter/cmd/docker-credential-porter/helper"
+)
+
+func main() {
+	credentials.Serve(&helper.PorterHelper{})
+}

BIN
dashboard/src/assets/aws-normal.png


+ 4 - 1
dashboard/src/main/home/Home.tsx

@@ -196,7 +196,10 @@ export default class Home extends Component<PropsType, StateType> {
         {this.renderSidebar()}
 
         <ViewWrapper>
-          <Navbar logOut={this.props.logOut} />
+          <Navbar 
+            logOut={this.props.logOut} 
+            currentView={this.state.currentView} // For form feedback
+          />
           {this.renderContents()}
         </ViewWrapper>
       </StyledHome>

+ 8 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -586,6 +586,14 @@ const CloseOverlay = styled.div`
   left: 0;
   width: 100%;
   height: 100%;
+  background: #202227;
+  animation: fadeIn 0.2s 0s;
+  opacity: 0;
+  animation-fill-mode: forwards;
+  @keyframes fadeIn {
+    from { opacity: 0 }
+    to { opacity: 1 }
+  }
 `;
 
 const HeaderWrapper = styled.div`

+ 255 - 0
dashboard/src/main/home/navbar/Feedback.tsx

@@ -0,0 +1,255 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import axios from 'axios';
+import { Context } from '../../../shared/Context';
+
+type PropsType = {
+  currentView: string,
+};
+
+type StateType = {
+  feedbackSent: boolean,
+  showFeedbackDropdown: boolean,
+  feedbackText: string,
+};
+
+export default class Feedback extends Component<PropsType, StateType> {
+  state = {
+    feedbackSent: false,
+    showFeedbackDropdown: false,
+    feedbackText: '',
+  }
+
+  renderReceipt = () => {
+    if (this.state.feedbackSent) {
+      return (
+        <DropdownAlt dropdownWidth='300px' dropdownMaxHeight='200px'>
+          <ConfirmationMessage>
+            <i className="material-icons-outlined">emoji_food_beverage</i>
+            Thanks for improving Porter.
+          </ConfirmationMessage>
+        </DropdownAlt>
+      );
+    }
+  }
+
+  handleSubmitFeedback = () => {
+    let { user } = this.context;
+    let msg = '👤 ' + user.email + ' 📍 ' + this.props.currentView + ': ' + this.state.feedbackText;
+    axios.post('http://35.190.59.124/feedback', {
+      key: 'uzNP7MVYqDC7hs9Q8YP7ehvsBO4yRO02ZGYQ5rKJ2YngEqgYVBITRsvDww8CfV3q',
+      cid: '794372152769642507',
+      message: msg,
+    }, {
+      headers: {
+        Authorization: `Bearer <>`
+      }
+    })
+    .then(res => {
+      console.log('feedback sent');
+    })
+    .catch(err => {
+      console.log(err);
+    });
+    this.setState({ feedbackSent: true, feedbackText: '' });
+  }
+
+  renderFeedbackDropdown = () => {
+    if (this.state.showFeedbackDropdown) {
+      let disabled = this.state.feedbackText === '';
+      return (
+        <>
+          <CloseOverlay onClick={() => this.setState({ showFeedbackDropdown: false, feedbackSent: false })} />
+          <Dropdown 
+            feedbackSent={this.state.feedbackSent} 
+            dropdownWidth='300px' 
+            dropdownMaxHeight='200px'
+          >
+            <FeedbackInput 
+              autoFocus={true}
+              value={this.state.feedbackText}
+              onChange={(e) => this.setState({ feedbackText: e.target.value })}
+              placeholder='Help us improve this page.' 
+            />
+            <SendButton 
+              disabled={disabled} 
+              onClick={() => !disabled && this.handleSubmitFeedback()}
+            >
+              <i className="material-icons">send</i> Send
+            </SendButton>
+          </Dropdown>
+          {this.renderReceipt()}
+        </>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <FeedbackButton>
+        <Flex onClick={() => this.setState({ showFeedbackDropdown: !this.state.showFeedbackDropdown })}>
+          <i className="material-icons-outlined">
+            campaign
+          </i>
+          Feedback?
+        </Flex>
+        {this.renderFeedbackDropdown()}
+      </FeedbackButton>
+    );
+  }
+}
+
+Feedback.contextType = Context;
+
+const CloseOverlay = styled.div`
+  position: fixed;
+  width: 100vw;
+  height: 100vh;
+  z-index: 100;
+  top: 0;
+  left: 0;
+  cursor: default;
+`;
+
+const ConfirmationMessage = styled.div`
+  width: 100%;
+  height: 100px;
+  display: flex;
+  font-size: 13px;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff55;
+
+  > i {
+    display: flex;
+    font-size: 16px;
+    margin-right: 10px;
+    align-items: center;
+    justify-content: center;
+    color: #ffffff55;
+  }
+`;
+
+const SendButton = styled.div`
+  display: flex;
+  align-items: center;
+  height: 40px;
+  cursor: ${(props: { disabled: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  justify-content: center;
+  margin-top: -3px;
+  font-size: 13px;
+  font-weight: 500;
+  font-family: 'Work Sans', sans-serif;
+  :hover {
+    background: ${(props: { disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+  }
+
+  > i {
+    background: none;
+    border-radius: 3px;
+    display: flex;
+    font-size: 14px;
+    top: 11px;
+    margin-right: 10px;
+    padding: 1px;
+    align-items: center;
+    justify-content: center;
+    color: #ffffffaa;
+  }
+`;
+
+const FeedbackInput = styled.textarea`
+  resize: none;
+  width: 100%;
+  height: 80px;
+  outline: 0;
+  padding: 14px;
+  color: white;
+  border: 0;
+  font-size: 13px;
+  font-family: 'Work Sans', sans-serif;
+  background: #aaaabb11;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+`;
+
+const Dropdown = styled.div`
+  position: absolute;
+  right: 0;
+  top: calc(100% + 5px);
+  background: #26282f;
+  width: ${(props: { dropdownWidth: string, dropdownMaxHeight: string, feedbackSent?: boolean }) => props.dropdownWidth};
+  max-height: ${(props: { dropdownWidth: string, dropdownMaxHeight: string, feedbackSent?: boolean }) => props.dropdownMaxHeight ? props.dropdownMaxHeight : '300px'};
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  margin-bottom: 20px;
+  box-shadow: 0 8px 20px 0px #00000088;
+  animation: ${(props: { dropdownWidth: string, dropdownMaxHeight: string, feedbackSent?: boolean }) => props.feedbackSent ? 'flyOff 0.3s 0.05s' : ''};
+  animation-fill-mode: forwards;
+  @keyframes flyOff {
+    from {
+      opacity: 1; transform: translateX(0px);
+    }
+    to {
+      opacity: 0; transform: translateX(100px);
+    }
+  }
+`;
+
+const DropdownAlt = styled(Dropdown)`
+  animation: fadeIn 0.3s 0.5s;
+  opacity: 0;
+  animation-fill-mode: forwards;
+  @keyframes fadeIn {
+    from { opacity: 0 }
+    to { opacity: 1 }
+  }
+`;
+
+const NavButton = styled.a`
+  display: flex;
+  position: relative;
+  align-items: center;
+  justify-content: center;
+  margin-right: 15px;
+  :hover {
+    > i {
+      color: #ffffff;
+    }
+  }
+  
+  > i {
+    cursor: pointer;
+    color: ${(props: { selected?: boolean }) => props.selected ? '#ffffff' : '#ffffff88'};
+    font-size: 24px;
+  }
+`;
+
+const FeedbackButton = styled(NavButton)`
+  color: ${(props: { selected?: boolean }) => props.selected ? '#ffffff' : '#ffffff88'};
+  font-family: 'Work Sans', sans-serif;
+  font-size: 14px;
+  margin-right: 20px;
+  :hover {
+    color: #ffffff;
+    > div {
+      > i {
+        color: #ffffff;
+      }
+    }
+  }
+
+  > div {
+    > i {
+      color: ${(props: { selected?: boolean }) => props.selected ? '#ffffff' : '#ffffff88'};
+      font-size: 26px;
+      margin-right: 6px;
+    }
+  }
+`;

+ 4 - 109
dashboard/src/main/home/navbar/Navbar.tsx

@@ -4,21 +4,20 @@ import styled from 'styled-components';
 import api from '../../../shared/api';
 import { Context } from '../../../shared/Context';
 
+import Feedback from './Feedback';
+
 type PropsType = {
   logOut: () => void,
+  currentView: string,
 };
 
 type StateType = {
   showDropdown: boolean,
-  showFeedbackDropdown: boolean,
-  feedbackSent: boolean,
 };
 
 export default class Navbar extends Component<PropsType, StateType> {
   state = {
     showDropdown: false,
-    showFeedbackDropdown: false,
-    feedbackSent: false,
   }
 
   handleLogout = (): void => {
@@ -47,48 +46,10 @@ export default class Navbar extends Component<PropsType, StateType> {
     }
   }
 
-  renderConfirmation = () => {
-    if (this.state.feedbackSent) {
-      return (
-        <DropdownAlt dropdownWidth='300px' dropdownMaxHeight='200px'>
-          <ConfirmationMessage>
-            <i className="material-icons-outlined">emoji_food_beverage</i>
-            Thanks for improving Porter.
-          </ConfirmationMessage>
-        </DropdownAlt>
-      );
-    }
-  }
-
-  renderFeedbackDropdown = () => {
-    if (this.state.showFeedbackDropdown) {
-      return (
-        <>
-          <CloseOverlay onClick={() => this.setState({ showFeedbackDropdown: false, feedbackSent: false })} />
-          <Dropdown feedbackSent={this.state.feedbackSent} dropdownWidth='300px' dropdownMaxHeight='200px'>
-            <FeedbackInput placeholder='Help us improve this page.' />
-            <SendButton onClick={() => this.setState({ feedbackSent: true })}>
-              <i className="material-icons">send</i> Send
-            </SendButton>
-          </Dropdown>
-          {this.renderConfirmation()}
-        </>
-      );
-    }
-  }
-
   render() {
     return (
       <StyledNavbar>
-        <FeedbackButton>
-          <Flex onClick={() => this.setState({ showFeedbackDropdown: !this.state.showFeedbackDropdown })}>
-            <i className="material-icons-outlined">
-              campaign
-            </i>
-            Feedback?
-          </Flex>
-          {this.renderFeedbackDropdown()}
-        </FeedbackButton>
+        <Feedback currentView={this.props.currentView} />
         <NavButton selected={this.state.showDropdown}>
           <i 
             className="material-icons-outlined" 
@@ -105,72 +66,6 @@ export default class Navbar extends Component<PropsType, StateType> {
 
 Navbar.contextType = Context;
 
-const ConfirmationMessage = styled.div`
-  width: 100%;
-  height: 100px;
-  display: flex;
-  font-size: 13px;
-  align-items: center;
-  justify-content: center;
-  color: #ffffff55;
-
-  > i {
-    display: flex;
-    font-size: 16px;
-    margin-right: 10px;
-    align-items: center;
-    justify-content: center;
-    color: #ffffff55;
-  }
-`;
-
-const SendButton = styled.div`
-  display: flex;
-  align-items: center;
-  height: 40px;
-  cursor: pointer;
-  justify-content: center;
-  margin-top: -3px;
-  font-size: 13px;
-  font-weight: 500;
-  font-family: 'Work Sans', sans-serif;
-  :hover {
-    background: #ffffff11;
-  }
-
-  > i {
-    background: none;
-    border-radius: 3px;
-    display: flex;
-    font-size: 14px;
-    top: 11px;
-    margin-right: 10px;
-    padding: 1px;
-    align-items: center;
-    justify-content: center;
-    color: #ffffffaa;
-  }
-`;
-
-const FeedbackInput = styled.textarea`
-  resize: none;
-  width: 100%;
-  height: 80px;
-  outline: 0;
-  padding: 14px;
-  color: white;
-  border: 0;
-  font-size: 13px;
-  font-family: 'Work Sans', sans-serif;
-  background: #aaaabb11;
-`;
-
-const Flex = styled.div`
-  display: flex;
-  align-items: center;
-  cursor: pointer;
-`;
-
 const CloseOverlay = styled.div`
   position: fixed;
   width: 100vw;

+ 1 - 0
dashboard/src/main/home/new-project/NewProject.tsx

@@ -213,6 +213,7 @@ export default class NewProject extends Component<PropsType, StateType> {
               let proj = res.data.find((el: ProjectType) => el.name === this.state.projectName);
               this.context.setCurrentProject(proj);
 
+              // Handle provisioning logic
               if (this.state.selectedProvider === 'aws') {
                 let clusterName = `${proj.name}-cluster`
 

+ 11 - 2
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -61,8 +61,17 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       _.set(values, key, rawValues[key]);
     }
 
-    _.set(values, "image.repository", this.state.selectedImageUrl)
-    _.set(values, "image.tag", this.state.selectedTag)
+    let imageUrl = this.state.selectedImageUrl;
+    let tag = this.state.selectedTag;
+
+    if (this.state.selectedImageUrl.includes(':')) {
+      let splits = this.state.selectedImageUrl.split(':');
+      imageUrl = splits[0];
+      tag = splits[1];
+    }
+
+    _.set(values, "image.repository", imageUrl)
+    _.set(values, "image.tag", tag)
 
     api.deployTemplate('<token>', {
       templateName: this.props.currentTemplate.name,

+ 3 - 1
go.mod

@@ -8,10 +8,12 @@ require (
 	github.com/Azure/go-autorest/autorest/adal v0.9.5 // indirect
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	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 // indirect
 	github.com/containerd/containerd v1.4.1 // indirect
 	github.com/coreos/rkt v1.30.0
 	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/evanphx/json-patch v4.9.0+incompatible // indirect
 	github.com/fatih/color v1.9.0

+ 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.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.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/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-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 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/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.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.4.0/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.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
 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/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 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-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-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-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190812172437-4e8604ab3aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

+ 67 - 0
server/api/registry_handler.go

@@ -10,6 +10,8 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/models"
+
+	"github.com/aws/aws-sdk-go/service/ecr"
 )
 
 // HandleCreateRegistry creates a new registry
@@ -95,6 +97,71 @@ func (app *App) HandleListProjectRegistries(w http.ResponseWriter, r *http.Reque
 	}
 }
 
+// temp -- token response
+type ECRTokenResponse struct {
+	Token string `json:"token"`
+}
+
+// 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
+	}
+
+	registryID, err := strconv.ParseUint(chi.URLParam(r, "registry_id"), 0, 64)
+
+	if err != nil || registryID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	reg, err := app.Repo.Registry.ReadRegistry(uint(registryID))
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// get the aws integration and session
+	awsInt, err := app.Repo.AWSIntegration.ReadAWSIntegration(reg.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
+	}
+
+	resp := &ECRTokenResponse{
+		Token: *output.AuthorizationData[0].AuthorizationToken,
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 // HandleUpdateProjectRegistry updates a registry
 func (app *App) HandleUpdateProjectRegistry(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)

+ 14 - 0
server/router/router.go

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