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

integrations frontend and registry boilerplate

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

+ 4 - 0
dashboard/src/assets/plus.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path opacity="0.4" d="M16.6667 2H7.33333C3.92889 2 2 3.92889 2 7.33333V16.6667C2 20.0622 3.92 22 7.33333 22H16.6667C20.0711 22 22 20.0622 22 16.6667V7.33333C22 3.92889 20.0711 2 16.6667 2Z" fill="white"/>
+<path d="M15.3205 12.7083H12.7495V15.257C12.7495 15.6673 12.4139 16 12 16C11.5861 16 11.2505 15.6673 11.2505 15.257V12.7083H8.67955C8.29342 12.6687 8 12.3461 8 11.9613C8 11.5765 8.29342 11.2539 8.67955 11.2143H11.2424V8.67365C11.2824 8.29088 11.6078 8 11.996 8C12.3842 8 12.7095 8.29088 12.7495 8.67365V11.2143H15.3205C15.7066 11.2539 16 11.5765 16 11.9613C16 12.3461 15.7066 12.6687 15.3205 12.7083Z" fill="white"/>
+</svg>

+ 17 - 3
dashboard/src/components/SaveButton.tsx

@@ -8,6 +8,9 @@ type PropsType = {
   disabled?: boolean,
   status?: string | null,
   color?: string,
+
+  // Makes flush with corner if not within a modal
+  makeFlush?: boolean 
 };
 
 type StateType = {
@@ -41,7 +44,7 @@ export default class SaveButton extends Component<PropsType, StateType> {
 
   render() {
     return (
-      <ButtonWrapper>
+      <ButtonWrapper makeFlush={this.props.makeFlush}>
         {this.renderStatus()}
         <Button 
           disabled={this.props.disabled}
@@ -93,8 +96,19 @@ const ButtonWrapper = styled.div`
   display: flex;
   align-items: center;
   position: absolute;
-  bottom: 25px;
-  right: 27px;
+  ${(props: { makeFlush: boolean }) => {
+    if (!props.makeFlush) {
+      return (`
+        bottom: 25px;
+        right: 27px;
+      `);
+    } 
+    return (`
+      bottom: 0;
+      right: 0;
+    `);
+  }}
+
 `;
 
 const Button = styled.button`

+ 239 - 0
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -0,0 +1,239 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import info from '../../assets/info.svg';
+
+import api from '../../shared/api';
+import { getRegistryIcon } from '../../shared/common';
+import { Context } from '../../shared/Context';
+
+import Loading from '../Loading';
+
+type PropsType = {
+  forceExpanded?: boolean,
+  selectedImageUrl: string | null,
+  setSelectedImageUrl: (x: string) => void
+};
+
+type StateType = {
+  isExpanded: boolean,
+  loading: boolean,
+  error: boolean,
+  images: any[]
+};
+
+const dummyImages = [
+  {
+    kind: 'docker-hub',
+    source: 'https://index.docker.io/jusrhee/image1',
+  },
+  {
+    kind: 'docker-hub',
+    source: 'https://index.docker.io/jusrhee/image2',
+  },
+  {
+    kind: 'docker-hub',
+    source: 'https://index.docker.io/jusrhee/image3',
+  },
+  {
+    kind: 'gcr',
+    source: 'https://gcr.io/some-registry/image1',
+  },
+  {
+    kind: 'gcr',
+    source: 'https://gcr.io/some-registry/image2',
+  },
+  {
+    kind: 'ecr',
+    source: 'https://aws_account_id.dkr.ecr.region.amazonaws.com/smth/1',
+  },
+  {
+    kind: 'ecr',
+    source: 'https://aws_account_id.dkr.ecr.region.amazonaws.com/smth/2',
+  },
+];
+
+export default class ImageSelector extends Component<PropsType, StateType> {
+  state = {
+    isExpanded: this.props.forceExpanded,
+    loading: false,
+    error: false,
+    images: [] as any[]
+  }
+
+  componentDidMount() {
+    this.setState({ images: dummyImages });
+  }
+
+  renderImageList = () => {
+    let { images, loading, error } = this.state;
+    if (loading) {
+      return <LoadingWrapper><Loading /></LoadingWrapper>
+    } else if (error || !images) {
+      return <LoadingWrapper>Error loading repos</LoadingWrapper>
+    }
+
+    return images.map((image: any, i: number) => {
+      let icon = getRegistryIcon(image.kind);
+      return (
+        <ImageItem
+          key={i}
+          isSelected={image.source === this.props.selectedImageUrl}
+          lastItem={i === images.length - 1}
+          onClick={() => this.props.setSelectedImageUrl(image.source)}
+        >
+          <img src={icon && icon} />{image.source}
+        </ImageItem>
+      );
+    });
+  }
+
+  renderExpanded = () => {
+    return (
+      <ExpandedWrapper>
+        {this.renderImageList()}
+      </ExpandedWrapper>
+    );
+  }
+
+  renderSelected = () => {
+    let { selectedImageUrl, setSelectedImageUrl } = this.props;
+    let icon = info;
+    return (
+      <Label>
+        <img src={icon} />
+        <Input
+          onClick={(e: any) => e.stopPropagation()}
+          value={selectedImageUrl}
+          onChange={(e: any) => setSelectedImageUrl(e.value)}
+          placeholder='Enter or select your container image URL'
+        />
+      </Label>
+    );
+  }
+
+  handleClick = () => {
+    if (!this.props.forceExpanded) {
+      this.setState({ isExpanded: !this.state.isExpanded });
+    }
+  }
+
+  render() {
+    return (
+      <div>
+        <StyledImageSelector
+          onClick={this.handleClick}
+          isExpanded={this.state.isExpanded}
+          forceExpanded={this.props.forceExpanded}
+        >
+          {this.renderSelected()}
+          {this.props.forceExpanded ? null : <i className="material-icons">{this.state.isExpanded ? 'close' : 'build'}</i>}
+        </StyledImageSelector>
+
+        {this.state.isExpanded ? this.renderExpanded() : null}
+      </div>
+    );
+  }
+}
+
+ImageSelector.contextType = Context;
+
+const Input = styled.input`
+  outline: 0;
+  background: none;
+  border: 0;
+  max-width: 800px;
+  min-width: 300px;
+  color: white;
+`;
+
+const ImageItem = styled.div`
+  display: flex;
+  width: 100%;
+  font-size: 13px;
+  border-bottom: 1px solid ${(props: { lastItem: boolean, isSelected: boolean }) => props.lastItem ? '#00000000' : '#606166'};
+  color: #ffffff;
+  user-select: none;
+  align-items: center;
+  padding: 10px 0px;
+  cursor: pointer;
+  background: ${(props: { isSelected: boolean, lastItem: boolean }) => props.isSelected ? '#ffffff22' : '#ffffff11'};
+  :hover {
+    background: #ffffff22;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+    filter: grayscale(100%);
+  }
+`;
+
+const LoadingWrapper = styled.div`
+  padding: 30px 0px;
+  background: #ffffff11;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  justify-content: center;
+  color: #ffffff44;
+`;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  border: 1px solid #ffffff44;
+  max-height: 275px;
+  overflow-y: auto;
+`;
+
+const Label = styled.div`
+  display: flex;
+  align-items: center;
+
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+  }
+`;
+
+const StyledImageSelector = styled.div`
+  width: 100%;
+  border: 1px solid #ffffff55;
+  background: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.isExpanded ? '#ffffff11' : ''};
+  border-radius: 3px;
+  user-select: none;
+  height: 40px;
+  font-size: 13px;
+  color: #ffffff;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: ${(props: { isExpanded: boolean, forceExpanded: boolean }) => props.forceExpanded ? '' : 'pointer'};;
+  :hover {
+    background: #ffffff11;
+
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > i {
+    font-size: 16px;
+    color: #ffffff66;
+    margin-right: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 20px;
+    padding: 4px;
+  }
+`;

+ 1 - 1
dashboard/src/components/values-form/InputRow.tsx

@@ -57,7 +57,7 @@ const Input = styled.input`
   border-radius: 3px;
   width: ${(props: { disabled: boolean, width: string }) => props.width ? props.width : '270px'};
   color: ${(props: { disabled: boolean, width: string }) => props.disabled ? '#ffffff44' : 'white'};
-  padding: 5px 8px;
+  padding: 5px 10px;
   margin-right: 8px;
   height: 30px;
 `;

+ 45 - 16
dashboard/src/main/home/Home.tsx

@@ -10,10 +10,12 @@ import Dashboard from './dashboard/Dashboard';
 import ClusterDashboard from './cluster-dashboard/ClusterDashboard';
 import Loading from '../../components/Loading';
 import Templates from './templates/Templates';
+import Integrations from "./integrations/Integrations";
 import LaunchTemplateModal from './modals/LaunchTemplateModal';
 import CreateProjectModal from './modals/CreateProjectModal';
 import UpdateProjectModal from './modals/UpdateProjectModal';
 import ClusterInstructionsModal from './modals/ClusterInstructionsModal';
+import IntegrationsModal from './modals/IntegrationsModal';
 
 type PropsType = {
   logOut: () => void
@@ -82,20 +84,17 @@ export default class Home extends Component<PropsType, StateType> {
   }
 
   renderContents = () => {
-    if (this.state.currentView === 'cluster-dashboard') {
+    let { currentView } = this.state;
+    if (currentView === 'cluster-dashboard') {
+      return this.renderDashboard();
+    } else if (currentView === 'dashboard') {
       return (
-        <StyledDashboard>
-          {this.renderDashboard()}
-        </StyledDashboard>
-      );
-    } else if (this.state.currentView === 'dashboard') {
-      return (
-        <StyledDashboard>
-          <DashboardWrapper>
-            <Dashboard />
-          </DashboardWrapper>
-        </StyledDashboard>
+        <DashboardWrapper>
+          <Dashboard />
+        </DashboardWrapper>
       );
+    } else if (currentView === 'integrations') {
+      return <Integrations />;
     }
 
     return <Templates />;
@@ -137,6 +136,14 @@ export default class Home extends Component<PropsType, StateType> {
         >
           <UpdateProjectModal />
         </ReactModal>
+        <ReactModal
+          isOpen={currentModal === 'IntegrationsModal'}
+          onRequestClose={() => setCurrentModal(null, null)}
+          style={SmallModalStyles}
+          ariaHideApp={false}
+        >
+          <IntegrationsModal />
+        </ReactModal>
 
         <Sidebar
           logOut={this.props.logOut}
@@ -145,8 +152,10 @@ export default class Home extends Component<PropsType, StateType> {
           setCurrentView={(x: string) => this.setState({ currentView: x })}
           currentView={this.state.currentView}
         />
-        
-        {this.renderContents()}
+
+        <ViewWrapper>
+          {this.renderContents()}
+        </ViewWrapper>
       </StyledHome>
     );
   }
@@ -173,6 +182,25 @@ const MediumModalStyles = {
   },
 };
 
+const SmallModalStyles = {
+  overlay: {
+    backgroundColor: 'rgba(0,0,0,0.6)',
+    zIndex: 2,
+  },
+  content: {
+    borderRadius: '7px',
+    border: 0,
+    width: '760px',
+    maxWidth: '80vw',
+    margin: '0 auto',
+    height: '425px',
+    top: 'calc(50% - 214px)',
+    backgroundColor: '#202227',
+    animation: 'floatInModal 0.5s 0s',
+    overflow: 'visible',
+  },
+};
+
 const ProjectModalStyles = {
   overlay: {
     backgroundColor: 'rgba(0,0,0,0.6)',
@@ -211,10 +239,10 @@ const TallModalStyles = {
   },
 };
 
-const StyledDashboard = styled.div`
+const ViewWrapper = styled.div`
   height: 100%;
   width: 100vw;
-  padding-top: 80px;
+  padding-top: 30px;
   overflow-y: auto;
   display: flex;
   flex: 1;
@@ -225,6 +253,7 @@ const StyledDashboard = styled.div`
 
 const DashboardWrapper = styled.div`
   width: 80%;
+  padding-top: 50px;
   min-width: 300px;
   padding-bottom: 120px;
 `;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -53,7 +53,7 @@ export default class ChartList extends Component<PropsType, StateType> {
         this.setState({ loading: false, error: true });
       } else {
         if (res.data) {
-          this.setState({ charts: res.data });
+          console.log(res.data);
         } else {
           this.setState({ charts: [] });
         }

+ 131 - 0
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -0,0 +1,131 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../shared/Context';
+import { getRegistryIcon } from '../../../shared/common';
+import api from '../../../shared/api';
+
+type PropsType = {
+  setCurrentIntegration: (x: any) => void
+};
+
+type StateType = {
+  integrations: any[]
+};
+
+const dummyIntegrations = [
+  {
+    name: 'docker-hub',
+    label: 'Docker Hub',
+  },
+  {
+    name: 'gcr',
+    label: 'Google Container Registry (GCR)',
+  },
+  {
+    name: 'ecr',
+    label: 'Amazon Elastic Container Registry (ECR)',
+  },
+];
+
+export default class IntegrationList extends Component<PropsType, StateType> {
+  state = {
+    integrations: [] as any[]
+  }
+
+  componentDidMount() {
+    this.setState({ integrations: dummyIntegrations });
+  }
+
+  renderContents = () => {
+    if (this.state.integrations) {
+      return this.state.integrations.map((integration: any, i: number) => {
+        let icon = getRegistryIcon(integration.name);
+        return (
+          <Integration
+            key={i}
+            onClick={() => this.props.setCurrentIntegration(integration)}
+          >
+            <Flex>
+              <Icon src={icon && icon} />
+              <Label>{integration.label}</Label>
+            </Flex>
+            <i className="material-icons">launch</i>
+          </Integration>
+        );
+      });
+    }
+    return (
+      <Placeholder>
+        You haven't set up any integrations yet.
+      </Placeholder>
+    );
+  }
+  
+  render() {
+    return ( 
+      <StyledIntegrationList>
+        {this.renderContents()}
+      </StyledIntegrationList>
+    );
+  }
+}
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const Integration = styled.div`
+  height: 70px;
+  width: calc(100% + 4px);
+  margin-left: -2px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25px;
+  cursor: pointer;
+  background: #26282f;
+  cursor: pointer;
+  margin-bottom: 15px;
+  border-radius: 5px;
+  box-shadow: 0 5px 8px 0px #00000033;
+  :hover {
+    background: #ffffff11;
+  }
+
+  > i {
+    font-size: 18px;
+    color: #616feecc;
+  }
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  font-size: 14px;
+  font-weight: 500;
+`;
+
+const Icon = styled.img`
+  width: 30px;
+  margin-right: 15px;
+`;
+
+const Placeholder = styled.div`
+  width: 100%;
+  height: 150px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  font-family: 'Work Sans', sans-serif;
+  justify-content: center;
+  margin-top: 30px;
+  background: #ffffff11;
+  color: #ffffff44;
+  border-radius: 5px;
+`;
+
+const StyledIntegrationList = styled.div`
+  margin-top: 20px;
+`;

+ 150 - 0
dashboard/src/main/home/integrations/Integrations.tsx

@@ -0,0 +1,150 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../shared/Context';
+import api from '../../../shared/api';
+import { getRegistryIcon } from '../../../shared/common';
+
+import IntegrationList from './IntegrationList';
+import DockerHubForm from './integration-forms/DockerHubForm';
+
+type PropsType = {
+};
+
+type StateType = {
+  currentIntegration: null | any
+};
+
+export default class Integrations extends Component<PropsType, StateType> {
+  state = {
+    currentIntegration: null as null | any,
+  }
+
+  renderContents = () => {
+    let { currentIntegration } = this.state;
+    if (currentIntegration) {
+      let icon = getRegistryIcon(currentIntegration.name);
+      return (
+        <div>
+          <TitleSectionAlt>
+            <Flex>
+              <i className="material-icons" onClick={() => this.setState({ currentIntegration: null })}>
+                keyboard_backspace
+              </i>
+              <Icon src={icon && icon} />
+              <Title>{currentIntegration.label}</Title>
+            </Flex>
+          </TitleSectionAlt>
+
+          <DockerHubForm />
+        </div>
+      );
+    }
+    return (
+      <div>
+        <TitleSection>
+          <Title>Integrations</Title>
+          <Button onClick={() => this.context.setCurrentModal('IntegrationsModal', {})}>
+            <i className="material-icons">add</i>
+            Add Integration
+          </Button>
+        </TitleSection>
+
+        <IntegrationList
+          setCurrentIntegration={(x: any) => this.setState({ currentIntegration: x })}
+        />
+      </div>
+    );
+  }
+  
+  render() {
+    return ( 
+      <StyledIntegrations>
+        {this.renderContents()}
+      </StyledIntegrations>
+    );
+  }
+}
+
+Integrations.contextType = Context;
+
+const Icon = styled.img`
+  width: 27px;
+  margin-right: 12px;
+  margin-bottom: -1px;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+
+  > i {
+    cursor: pointer;
+    font-size 24px;
+    color: #969Fbbaa;
+    padding: 3px;
+    margin-right: 11px;
+    border-radius: 100px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
+const Button = styled.div`
+  height: 100%;
+  background: #616feecc;
+  :hover {
+    background: #505edddd;
+  }
+  color: white;
+  font-weight: 500;
+  font-size: 13px;
+  padding: 10px 15px;
+  border-radius: 3px;
+  cursor: pointer;
+  box-shadow: 0 5px 8px 0px #00000010;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  > img, i {
+    width: 20px;
+    height: 20px;
+    font-size: 16px;
+    display: flex;
+    align-items: center;
+    margin-right: 10px;
+    justify-content: center;
+  }
+`;
+
+const Title = styled.div`
+  font-size: 24px;
+  font-weight: 600;
+  font-family: 'Work Sans', sans-serif;
+  color: #ffffff;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const TitleSection = styled.div`
+  margin-bottom: 20px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  height: 40px;
+`;
+
+const TitleSectionAlt = styled(TitleSection)`
+  margin-left: -42px;
+  width: calc(100% + 42px);
+`;
+
+const StyledIntegrations = styled.div`
+  width: calc(90% - 150px);
+  min-width: 300px;
+  padding-top: 45px;
+`;

+ 84 - 0
dashboard/src/main/home/integrations/integration-forms/DockerHubForm.tsx

@@ -0,0 +1,84 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import { Context } from '../../../../shared/Context';
+import api from '../../../../shared/api';
+
+import InputRow from '../../../../components/values-form/InputRow';
+import SaveButton from '../../../../components/SaveButton';
+
+type PropsType = {
+};
+
+type StateType = {
+  registryURL: string,
+  dockerEmail: string,
+  dockerUsername: string,
+  dockerPassword: string
+};
+
+export default class DockerHubForm extends Component<PropsType, StateType> {
+  state = {
+    registryURL: '',
+    dockerEmail: '',
+    dockerUsername: '',
+    dockerPassword: ''
+  }
+
+  render() {
+    return ( 
+      <StyledDockerHubForm>
+        <CredentialWrapper>
+          <InputRow
+            type='text'
+            value={this.state.registryURL}
+            setValue={(x: string) => this.setState({ registryURL: x })}
+            label='📦 Registry URL'
+            placeholder='ex: index.docker.io'
+            width='100%'
+          />
+          <InputRow
+            type='text'
+            value={this.state.dockerEmail}
+            setValue={(x: string) => this.setState({ dockerEmail: x })}
+            label='✉️ Docker Email'
+            placeholder='ex: captain@ahab.com'
+            width='100%'
+          />
+          <InputRow
+            type='text'
+            value={this.state.dockerUsername}
+            setValue={(x: string) => this.setState({ dockerUsername: x })}
+            label='👤 Docker Username'
+            placeholder='ex: whale_watcher_2000'
+            width='100%'
+          />
+          <InputRow
+            type='password'
+            value={this.state.dockerPassword}
+            setValue={(x: string) => this.setState({ dockerPassword: x })}
+            label='🔒 Docker Password'
+            placeholder='○ ○ ○ ○ ○ ○ ○ ○ ○'
+            width='100%'
+          />
+        </CredentialWrapper>
+        <SaveButton
+          text='Save Changes'
+          makeFlush={true}
+          onClick={() => console.log('unimplemented')}
+        />
+      </StyledDockerHubForm>
+    );
+  }
+}
+
+const CredentialWrapper = styled.div`
+  padding: 10px 40px 25px;
+  background: #ffffff11;
+  border-radius: 5px;
+`;
+
+const StyledDockerHubForm = styled.div`
+  position: relative;
+  padding-bottom: 75px;
+`;

+ 165 - 0
dashboard/src/main/home/modals/IntegrationsModal.tsx

@@ -0,0 +1,165 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+import close from '../../../assets/close.png';
+
+import { Context } from '../../../shared/Context';
+import { getRegistryIcon } from '../../../shared/common';
+
+type PropsType = {
+};
+
+type StateType = {
+  integrations: any[]
+};
+
+const dummyIntegrations = [
+  {
+    name: 'docker-hub',
+    label: 'Docker Hub',
+  },
+  {
+    name: 'gcr',
+    label: 'Google Container Registry (GCR)',
+  },
+  {
+    name: 'ecr',
+    label: 'Amazon Elastic Container Registry (ECR)',
+  },
+];
+
+export default class IntegrationsModal extends Component<PropsType, StateType> {
+  state = {
+    currentTab: 'mac',
+    integrations: [] as any[]
+  }
+
+  componentDidMount() {
+    this.setState({ integrations: dummyIntegrations });
+  }
+
+  renderIntegrationsCatalog = () => {
+    return this.state.integrations.map((integration: any, i: number) => {
+      let icon = getRegistryIcon(integration.name);
+      return (
+        <IntegrationOption key={i}>
+          <Icon src={icon && icon} />
+          <Label>{integration.label}</Label>
+        </IntegrationOption>
+      );
+    });
+  }
+ 
+  render() {
+    return (
+      <StyledIntegrationsModal>
+        <CloseButton onClick={() => {
+          this.context.setCurrentModal(null, null);
+        }}>
+          <CloseButtonImg src={close} />
+        </CloseButton>
+
+        <ModalTitle>Add a New Integration</ModalTitle>
+        <Subtitle>Select the service you would like to connect to.</Subtitle>
+       
+        <IntegrationsCatalog>
+          {this.renderIntegrationsCatalog()}
+        </IntegrationsCatalog>
+      </StyledIntegrationsModal>
+    );
+  }
+}
+
+IntegrationsModal.contextType = Context;
+
+const Label = styled.div`
+  color: #ffffff;
+  font-size: 14px;
+  font-weight: 500;
+`;
+
+const Icon = styled.img`
+  width: 30px;
+  margin-right: 15px;
+`;
+
+const IntegrationOption = styled.div`
+  height: 60px;
+  width: 100%;
+  border-bottom: 1px solid #ffffff44;
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const IntegrationsCatalog = styled.div`
+  width: 100%;
+  margin-top: 17px;
+  border: 1px solid #ffffff44;
+  border-radius: 5px;
+  background: #ffffff11;
+  height: calc(100% - 100px);
+  overflow-y: auto;
+`;
+
+const Subtitle = styled.div`
+  padding: 10px 0px;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const ModalTitle = styled.div`
+  margin: 0px 0px 13px;
+  display: flex;
+  flex: 1;
+  font-family: 'Assistant';
+  font-size: 18px;
+  color: #ffffff;
+  user-select: none;
+  font-weight: 700;
+  align-items: center;
+  position: relative;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
+const CloseButton = styled.div`
+  position: absolute;
+  display: block;
+  width: 40px;
+  height: 40px;
+  padding: 13px 0 12px 0;
+  z-index: 1;
+  text-align: center;
+  border-radius: 50%;
+  right: 15px;
+  top: 12px;
+  cursor: pointer;
+  :hover {
+    background-color: #ffffff11;
+  }
+`;
+
+const CloseButtonImg = styled.img`
+  width: 14px;
+  margin: 0 auto;
+`;
+
+const StyledIntegrationsModal= styled.div`
+  width: 100%;
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100%;
+  padding: 25px 32px;
+  overflow: hidden;
+  border-radius: 6px;
+  background: #202227;
+`;

+ 4 - 1
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -123,7 +123,10 @@ export default class Sidebar extends Component<PropsType, StateType> {
             <img src={filter} />
             Templates
           </NavButton>
-          <NavButton disabled={true}>
+          <NavButton
+            onClick={() => this.props.setCurrentView('integrations')}
+            selected={this.props.currentView === 'integrations'}
+          >
             <img src={integrations} />
             Integrations
           </NavButton>

+ 2 - 18
dashboard/src/main/home/templates/Templates.tsx

@@ -118,11 +118,7 @@ export default class Templates extends Component<PropsType, StateType> {
   }
   
   render() {
-    return ( 
-      <StyledTemplates>
-        {this.renderContents()}
-      </StyledTemplates>
-    );
+    return this.renderContents();
   }
 }
 
@@ -186,7 +182,6 @@ const TemplateTitle = styled.div`
 `;
 
 const TemplateBlock = styled.div`
-  background: none;
   border: 1px solid #ffffff00;
   align-items: center;
   user-select: none;
@@ -242,19 +237,8 @@ const TitleSection = styled.div`
   align-items: center;
 `;
 
-const StyledTemplates = styled.div`
-  height: 100%;
-  width: 100vw;
-  padding-top: 45px;
-  overflow-y: auto;
-  display: flex;
-  flex: 1;
-  justify-content: center;
-  position: relative;
-`;
-
 const TemplatesWrapper = styled.div`
   width: calc(90% - 150px);
   min-width: 300px;
-  padding-top: 30px;
+  padding-top: 50px;
 `;

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

@@ -51,5 +51,5 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
 const StyledExpandedTemplate = styled.div`
   width: calc(90% - 150px);
   min-width: 300px;
-  padding-top: 30px;
+  padding-top: 50px;
 `;

+ 26 - 6
dashboard/src/main/home/templates/expanded-template/LaunchTemplate.tsx

@@ -6,6 +6,7 @@ import api from '../../../../shared/api';
 
 import { PorterChart, RepoType, Cluster } from '../../../../shared/types';
 import Selector from '../../../../components/Selector';
+import ImageSelector from '../../../../components/image-selector/ImageSelector';
 
 type PropsType = {
   currentTemplate: PorterChart,
@@ -16,9 +17,7 @@ type StateType = {
   currentView: string,
   clusterOptions: { label: string, value: string }[],
   selectedCluster: string,
-  selectedRepo: RepoType | null,
-  selectedBranch: string,
-  subdirectory: string,
+  selectedImageUrl: string | null,
 };
 
 export default class LaunchTemplate extends Component<PropsType, StateType> {
@@ -26,9 +25,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     currentView: 'repo',
     clusterOptions: [] as { label: string, value: string }[],
     selectedCluster: this.context.currentCluster.name,
-    selectedRepo: null as RepoType | null,
-    selectedBranch: '',
-    subdirectory: '',
+    selectedImageUrl: null as string | null,
   };
 
   componentDidMount() {
@@ -90,6 +87,16 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
             closeOverlay={true}
           />
         </ClusterSection>
+
+        <Subtitle>Select the container image you would like to connect to this template.</Subtitle>
+        <ImageSelector
+          selectedImageUrl={this.state.selectedImageUrl}
+          setSelectedImageUrl={(x: string) => this.setState({ selectedImageUrl: x })}
+          forceExpanded={true}
+        />
+
+        <br />
+        <Subtitle>Configure additional settings for this template (optional).</Subtitle>
       </StyledLaunchTemplate>
     );
   }
@@ -97,6 +104,18 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 LaunchTemplate.contextType = Context;
 
+const Subtitle = styled.div`
+  padding: 12px 0px 25px;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  margin-top: 3px;
+  margin-bottom: 5px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+`;
+
 const ClusterLabel = styled.div`
   margin-right: 10px;
   display: flex;
@@ -137,6 +156,7 @@ const ClusterSection = styled.div`
   font-size: 14px;
   font-weight: 500;
   margin-top: 20px;
+  margin-bottom: 15px;
 
   > i {
     font-size: 25px;

+ 1 - 1
dashboard/src/main/home/templates/expanded-template/TemplateInfo.tsx

@@ -143,7 +143,7 @@ const Button = styled.div`
   font-size: 13px;
   padding: 10px 15px;
   border-radius: 3px;
-  cursor: ${(props: { isDisabled: boolean }) => (!props.isDisabled ? 'pointer' : 'default')};;
+  cursor: ${(props: { isDisabled: boolean }) => (!props.isDisabled ? 'pointer' : 'default')};
   box-shadow: 0 5px 8px 0px #00000010;
   display: flex;
   flex-direction: row;

+ 12 - 0
dashboard/src/shared/common.tsx

@@ -0,0 +1,12 @@
+export const getRegistryIcon = (kind: string) => {
+  switch (kind) {
+    case 'docker-hub':
+      return 'https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png';
+    case 'gcr':
+      return 'https://carlossanchez.files.wordpress.com/2019/06/21046548.png?w=640';
+    case 'ecr':
+      return 'https://avatars2.githubusercontent.com/u/52505464?s=400&u=da920f994c67665c7ad6c606a5286557d4f8555f&v=4';
+    default:
+      return null
+  }
+}

+ 3 - 3
go.mod

@@ -7,7 +7,7 @@ require (
 	github.com/Azure/go-autorest/autorest v0.11.1 // 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.30.0
+	github.com/aws/aws-sdk-go v1.31.6
 	github.com/containerd/containerd v1.4.1
 	github.com/cosmtrek/air v1.21.2 // indirect
 	github.com/creack/pty v1.1.11 // indirect
@@ -25,6 +25,7 @@ require (
 	github.com/go-playground/validator/v10 v10.3.0
 	github.com/go-test/deep v1.0.7
 	github.com/google/go-cmp v0.5.1
+	github.com/google/go-containerregistry v0.1.4
 	github.com/google/go-github v17.0.0+incompatible
 	github.com/google/go-github/v32 v32.1.0
 	github.com/gorilla/securecookie v1.1.1
@@ -45,14 +46,13 @@ require (
 	github.com/spf13/cobra v1.0.0
 	github.com/spf13/viper v1.4.0
 	github.com/stretchr/testify v1.6.1
-	golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
+	golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
 	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
 	golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6
 	golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
 	google.golang.org/api v0.30.0
 	google.golang.org/genproto v0.0.0-20201014134559-03b6142f0dc9
 	google.golang.org/grpc v1.33.0 // indirect
-	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	gopkg.in/go-playground/validator.v9 v9.31.0
 	gopkg.in/yaml.v2 v2.3.0
 	gorm.io/driver/postgres v1.0.2

+ 34 - 0
go.sum

@@ -52,6 +52,8 @@ github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
 github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go v42.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0=
 github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
@@ -62,12 +64,14 @@ github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSW
 github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
 github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
 github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
+github.com/Azure/go-autorest/autorest v0.10.2/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
 github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
 github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
 github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
 github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
 github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
 github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
+github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
 github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
 github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
 github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
@@ -77,8 +81,11 @@ github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxB
 github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
 github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
 github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
 github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc=
+github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA=
 github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8=
+github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=
 github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
@@ -112,6 +119,7 @@ github.com/Masterminds/squirrel v1.4.0 h1:he5i/EXixZxrBUWcxzDYMiju9WZ3ld/l7QBNuo
 github.com/Masterminds/squirrel v1.4.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
 github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
 github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
 github.com/Microsoft/hcsshim v0.0.0-20190417211021-672e52e9209d/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
 github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
@@ -176,6 +184,8 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
 github.com/aws/aws-sdk-go v1.28.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 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=
@@ -253,6 +263,7 @@ github.com/containerd/console v0.0.0-20170925154832-84eeaae905fa/go.mod h1:Tj/on
 github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
 github.com/containerd/containerd v1.0.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
@@ -312,12 +323,14 @@ github.com/digitalocean/godo v1.19.0/go.mod h1:AAPQ+tiM4st79QHlEBTg8LM7JQNre4SAQ
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/dlespiau/kube-test-harness v0.0.0-20190930170435-ec3f93e1a754/go.mod h1:rTr8X4qZPRmQKsyAjhECPi+zPnmlcmv5W9s1F11oBSo=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492 h1:FwssHbCDJD025h+BchanCwE1Q8fyMgqDr2mOQAWOLGw=
 github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
 github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce h1:KXS1Jg+ddGcWA8e1N7cupxaHHZhit5rB9tfDU+mfjyY=
 github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
@@ -376,6 +389,7 @@ github.com/fluxcd/flux/pkg/install v0.0.0-20201001122558-cb08da1b356a/go.mod h1:
 github.com/fluxcd/go-git-providers v0.0.3/go.mod h1:iaXf3nEq8MB/LzxfbNcCl48sAtIReUU7jqjJ7CEnfFQ=
 github.com/fluxcd/helm-operator/pkg/install v0.0.0-20200729150005-1467489f7ee4/go.mod h1:ijsiZLK3c4Qu4sFqHu5pJdwjmMEjvKpwivq3uAdffBk=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
@@ -585,6 +599,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-containerregistry v0.1.4 h1:fZm+V2pYnvb8NMPM1YOsyxr31XKfpHTun5oVTRnG8qc=
+github.com/google/go-containerregistry v0.1.4/go.mod h1:6EGiuQp36pL82lX6rFN0s9AJOVL0Mlgx/DAsYZW5X3s=
 github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
@@ -628,6 +644,8 @@ github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UE
 github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
 github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
+github.com/googleapis/gnostic v0.2.2 h1:DcFegQ7+ECdmkJMfVwWlC+89I4esJ7p8nkGt9ainGDk=
+github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
 github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
 github.com/gophercloud/gophercloud v0.0.0-20180807015416-4ea085781bae/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
 github.com/gophercloud/gophercloud v0.0.0-20190216224116-dcc6e84aef1b/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
@@ -691,6 +709,8 @@ github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
+github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hashicorp/hcl v0.0.0-20160711231752-d8c773c4cba1/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
 github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -794,6 +814,7 @@ 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.1-0.20190826204134-d7d95172beb5 h1:lrdPtrORjGv1HbbEvKWDUAy97mPpFm4B8hp77tcCUJY=
 github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
+github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
 github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd h1:nIzoSW6OhhppWLm4yqBwZsKJlAayUu5FGozhrF3ETSM=
 github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd/go.mod h1:MEQrHur0g8VplbLOv5vXmDzacSaH9Z7XhcgsSh1xciU=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
@@ -924,6 +945,7 @@ github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb44
 github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
 github.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV59fDr4=
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=
@@ -1144,6 +1166,7 @@ github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 h1:fOCp11H0yuyAt2wql
 github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522/go.mod h1:tQTYKOQgxoH3v6dEmdHiz4JG+nbxWwM5fgPQUpSZqVQ=
 github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
 github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE=
@@ -1279,6 +1302,7 @@ github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk
 github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
 github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
+github.com/vdemeester/k8s-pkg-credentialprovider v1.18.1-0.20201019120933-f1d16962a4db/go.mod h1:grWy0bkr1XO6hqbaaCKaPXqkBVlMGHYG6PGykktwbJc=
 github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
 github.com/vektra/mockery v0.0.0-20181123154057-e78b021dcbb5/go.mod h1:ppEjwdhyy7Y31EnHRDm1JkChoC7LXIJ7Ex0VYLWtZtQ=
 github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
@@ -1333,6 +1357,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
 github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
 github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
@@ -1403,6 +1428,8 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1543,6 +1570,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/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=
@@ -1593,6 +1621,7 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1629,6 +1658,7 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw
 golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
 golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
 golang.org/x/tools v0.0.0-20190812233024-afc3694995b6/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1679,6 +1709,7 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1750,6 +1781,7 @@ google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
@@ -1870,6 +1902,7 @@ k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwn
 k8s.io/cloud-provider v0.18.8/go.mod h1:cn9AlzMPVIXA4HHLVbgGUigaQlZyHSZ7WAwDEFNrQSs=
 k8s.io/cluster-bootstrap v0.18.8/go.mod h1:guq0Uc+QwazHgpS1yAw5Z7yUlBCtGppbgWQkbN3lxIY=
 k8s.io/code-generator v0.16.8/go.mod h1:wFdrXdVi/UC+xIfLi+4l9elsTT/uEF61IfcN2wOLULQ=
+k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
 k8s.io/code-generator v0.18.8/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
 k8s.io/component-base v0.16.8/go.mod h1:Q8UWOWShpP3MZZny4n/15gOncfaaVtc9SbCdkM5MhUE=
 k8s.io/component-base v0.18.8 h1:BW5CORobxb6q5mb+YvdwQlyXXS6NVH5fDXWbU7tf2L8=
@@ -1897,6 +1930,7 @@ k8s.io/kube-aggregator v0.18.8/go.mod h1:CyLoGZB+io8eEwnn+6RbV7QWJQhj8a3TBH8ZM8s
 k8s.io/kube-controller-manager v0.18.8/go.mod h1:IYZteddXJFD1TVgAw8eRP3c9OOA2WtHdXdE8aH6gXnc=
 k8s.io/kube-openapi v0.0.0-20180509051136-39cb288412c4/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
 k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
+k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
 k8s.io/kube-proxy v0.18.8/go.mod h1:u4E8OsUpUzfZ9CEFf9rdLsbYiusZr8utbtF4WQrX+qs=

+ 41 - 0
server/api/registry_handler.go

@@ -0,0 +1,41 @@
+package api
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/google/go-containerregistry/pkg/authn"
+	"github.com/google/go-containerregistry/pkg/name"
+	"github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+// HandleListImages retrieves a list of repo names
+func (app *App) HandleListImages(w http.ResponseWriter, r *http.Request) {
+	ref, err := name.ParseReference("gcr.io/google-containers/pause")
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	img, err := remote.Image(ref)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(img.Size())
+
+	ctx := r.Context()
+	reg, err := name.NewRegistry("index.docker.io")
+	if err != nil {
+		fmt.Println("fuk")
+		fmt.Println(err)
+		return
+	}
+
+	stuff, err := remote.Catalog(ctx, reg, remote.WithAuthFromKeychain(authn.DefaultKeychain))
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(stuff[0])
+}

+ 113 - 0
server/api/registry_handler_test.go

@@ -0,0 +1,113 @@
+package api_test
+
+import (
+	"encoding/json"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/porter-dev/porter/internal/kubernetes"
+)
+
+// ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
+
+type imagesTest struct {
+	initializers []func(tester *tester)
+	msg          string
+	method       string
+	endpoint     string
+	body         string
+	expStatus    int
+	expBody      string
+	useCookie    bool
+	validators   []func(c *imagesTest, tester *tester, t *testing.T)
+}
+
+func testImagesRequests(t *testing.T, tests []*imagesTest, 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 listImagesTests = []*imagesTest{
+	&imagesTest{
+		initializers: []func(tester *tester){
+			initDefaultImages,
+		},
+		msg:       "List images",
+		method:    "GET",
+		endpoint:  "/api/projects/1/images",
+		body:      "",
+		expStatus: http.StatusOK,
+		expBody:   "unimplemented",
+		useCookie: true,
+		validators: []func(c *imagesTest, tester *tester, t *testing.T){
+			imagesListValidator,
+		},
+	},
+}
+
+func TestHandleListImages(t *testing.T) {
+	testImagesRequests(t, listImagesTests, true)
+}
+
+// ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
+
+func initDefaultImages(tester *tester) {
+	initUserDefault(tester)
+
+	agent := kubernetes.GetAgentTesting(defaultObjects...)
+
+	// overwrite the test agent with new resources
+	tester.app.TestAgents.K8sAgent = agent
+}
+
+func imagesListValidator(c *imagesTest, tester *tester, t *testing.T) {
+	var gotBody map[string]interface{}
+	var expBody map[string]interface{}
+
+	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
+	json.Unmarshal([]byte(c.expBody), &expBody)
+
+	if string(tester.rr.Body.Bytes()) != c.expBody {
+		t.Errorf("Mismatch")
+	}
+}

+ 8 - 0
server/router/router.go

@@ -250,6 +250,14 @@ func New(
 			),
 		)
 
+		// /api/projects/{project_id}/images routes
+		// TODO: add back project access check
+		r.Method(
+			"GET",
+			"/projects/{project_id}/images",
+			auth.BasicAuthenticate(requestlog.NewHandler(a.HandleListImages, l)),
+		)
+
 		// /api/templates routes
 		r.Method(
 			"GET",