Explorar el Código

image tag selector for jobs, chronological for ecr

jusrhee hace 5 años
padre
commit
ff9208e116

+ 3 - 7
dashboard/src/components/image-selector/ImageList.tsx

@@ -18,6 +18,7 @@ type PropsType = {
   setSelectedImageUrl: (x: string) => void;
   setSelectedTag: (x: string) => void;
   setClickedImage: (x: ImageType) => void;
+  disableImageSelect?: boolean;
 };
 
 type StateType = {
@@ -162,11 +163,6 @@ export default class ImageList extends Component<PropsType, StateType> {
     }
   }
 
-  /*
-  <Highlight onClick={() => this.props.setCurrentView('integrations')}>
-    Link your registry.
-  </Highlight>
-  */
   renderImageList = () => {
     let { images, loading, error } = this.state;
 
@@ -206,8 +202,8 @@ export default class ImageList extends Component<PropsType, StateType> {
   };
 
   renderBackButton = () => {
-    let { setSelectedImageUrl } = this.props;
-    if (this.props.clickedImage) {
+    let { setSelectedImageUrl, clickedImage, disableImageSelect } = this.props;
+    if (clickedImage && !disableImageSelect) {
       return (
         <BackButton
           width="175px"

+ 5 - 101
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -17,6 +17,7 @@ type PropsType = {
   setSelectedImageUrl: (x: string) => void;
   setSelectedTag: (x: string) => void;
   noTagSelection?: boolean;
+  disableImageSelect?: boolean;
 };
 
 type StateType = {
@@ -36,87 +37,6 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     clickedImage: null as ImageType | null,
   };
 
-  // componentDidMount() {
-  //   const { currentProject, setCurrentError } = this.context;
-  //   let images = [] as ImageType[];
-  //   let errors = [] as number[];
-  //   api
-  //     .getProjectRegistries("<token>", {}, { id: currentProject.id })
-  //     .then(async (res) => {
-  //       let registries = res.data;
-  //       if (registries.length === 0) {
-  //         this.setState({ loading: false });
-  //       }
-
-  //       // Loop over connected image registries
-  //       registries.forEach(async (registry: any, i: number) => {
-  //         await new Promise((nextController: (res?: any) => void) => {
-  //           api
-  //             .getImageRepos(
-  //               "<token>",
-  //               {},
-  //               {
-  //                 project_id: currentProject.id,
-  //                 registry_id: registry.id,
-  //               }
-  //             )
-  //             .then((res) => {
-  //               res.data.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
-  //               // Loop over found image repositories
-  //               let newImg = res.data.map((img: any) => {
-  //                 if (this.props.selectedImageUrl === img.uri) {
-  //                   this.setState({
-  //                     clickedImage: {
-  //                       kind: registry.service,
-  //                       source: img.uri,
-  //                       name: img.name,
-  //                       registryId: registry.id,
-  //                     },
-  //                   });
-  //                 }
-  //                 return {
-  //                   kind: registry.service,
-  //                   source: img.uri,
-  //                   name: img.name,
-  //                   registryId: registry.id,
-  //                 };
-  //               });
-  //               images.push(...newImg);
-  //               errors.push(0);
-  //             })
-  //             .catch(() => errors.push(1))
-  //             .finally(() => {
-  //               if (i == registries.length - 1) {
-  //                 let error =
-  //                   errors.reduce((a, b) => {
-  //                     return a + b;
-  //                   }) == registries.length
-  //                     ? true
-  //                     : false;
-
-  //                 this.setState({
-  //                   images,
-  //                   loading: false,
-  //                   error,
-  //                 });
-  //               }
-
-  //               nextController();
-  //             });
-  //         });
-  //       });
-  //     })
-  //     .catch((err) => {
-  //       console.log(err);
-  //       this.setState({ error: true });
-  //     });
-  // }
-
-  /*
-  <Highlight onClick={() => this.props.setCurrentView('integrations')}>
-    Link your registry.
-  </Highlight>
-  */
   renderImageList = () => {
     let { images, loading, error } = this.state;
 
@@ -155,24 +75,6 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     });
   };
 
-  renderBackButton = () => {
-    let { setSelectedImageUrl } = this.props;
-    if (this.state.clickedImage) {
-      return (
-        <BackButton
-          width="175px"
-          onClick={() => {
-            setSelectedImageUrl("");
-            this.setState({ clickedImage: null });
-          }}
-        >
-          <i className="material-icons">keyboard_backspace</i>
-          Select Image Repo
-        </BackButton>
-      );
-    }
-  };
-
   renderSelected = () => {
     let { selectedImageUrl, setSelectedImageUrl } = this.props;
     let { clickedImage } = this.state;
@@ -192,6 +94,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
       <Label>
         <img src={icon} />
         <Input
+          disabled={this.props.disableImageSelect}
           autoFocus={true}
           onClick={(e: any) => e.stopPropagation()}
           value={selectedImageUrl}
@@ -233,6 +136,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
 
         {this.state.isExpanded ? (
           <ImageList
+            disableImageSelect={this.props.disableImageSelect}
             selectedImageUrl={this.props.selectedImageUrl}
             selectedTag={this.props.selectedTag}
             clickedImage={this.state.clickedImage}
@@ -284,13 +188,13 @@ const BackButton = styled.div`
   }
 `;
 
-const Input = styled.input`
+const Input = styled.input<{ disabled: boolean }>`
   outline: 0;
   background: none;
   border: 0;
   font-size: 13px;
   width: calc(100% - 60px);
-  color: white;
+  color: ${(props) => (props.disabled ? "#aaaabb" : "#ffffff")};
 `;
 
 const ImageItem = styled.div`

+ 54 - 3
dashboard/src/components/image-selector/TagList.tsx

@@ -32,7 +32,8 @@ export default class TagList extends Component<PropsType, StateType> {
     currentTag: this.props.selectedTag,
   };
 
-  componentDidMount() {
+  refreshTagList = () => {
+    this.setState({ loading: true });
     const { currentProject } = this.context;
 
     let splits = this.props.selectedImageUrl.split("/");
@@ -55,6 +56,14 @@ export default class TagList extends Component<PropsType, StateType> {
         }
       )
       .then((res) => {
+        // Sort if timestamp is available
+        if (res.data.length > 0 && res.data[0].pushed_at) {
+          res.data.sort((a: any, b: any) => {
+            let d1 = new Date(a.pushed_at);
+            let d2 = new Date(b.pushed_at);
+            return d2.getTime() - d1.getTime();
+          });
+        }
         let tags = res.data.map((tag: any, i: number) => {
           return tag.tag;
         });
@@ -64,6 +73,10 @@ export default class TagList extends Component<PropsType, StateType> {
         console.log(err);
         this.setState({ loading: false, error: true });
       });
+  };
+
+  componentDidMount() {
+    this.refreshTagList();
   }
 
   setTag = (tag: string) => {
@@ -105,7 +118,12 @@ export default class TagList extends Component<PropsType, StateType> {
     return (
       <>
         <TagNameAlt>
-          <img src={info} /> Select Image Tag
+          <Label>
+            <img src={info} /> Select Image Tag
+          </Label>
+          <Refresh onClick={this.refreshTagList}>
+            <i className="material-icons">autorenew</i> Refresh
+          </Refresh>
         </TagNameAlt>
         <StyledTagList>{this.renderTagList()}</StyledTagList>
       </>
@@ -115,6 +133,36 @@ export default class TagList extends Component<PropsType, StateType> {
 
 TagList.contextType = Context;
 
+const Label = styled.div`
+  display: flex;
+  align-items: center;
+  > img {
+    width: 18px;
+    height: 18px;
+    margin-left: 12px;
+    margin-right: 12px;
+  }
+`;
+
+const Refresh = styled.div`
+  margin-right: 10px;
+  cursor: pointer;
+  color: #949eff;
+  display: flex;
+  align-items: center;
+  font-weight: 500;
+  border-radius: 3px;
+  padding: 2px 3px;
+  padding-right: 7px;
+  > i {
+    font-size: 17px;
+    margin-right: 6px;
+  }
+  :hover {
+    background: #ffffff11;
+  }
+`;
+
 const StyledTagList = styled.div`
   max-height: 175px;
   position: relative;
@@ -152,10 +200,13 @@ const TagName = styled.div`
 `;
 
 const TagNameAlt = styled(TagName)`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
   color: #ffffff55;
   cursor: default;
   :hover {
-    background: #ffffff11;
+    background: none;
     > i {
       background: none;
     }

+ 5 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -308,7 +308,8 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
         {
           ...(this.state.currentChart.config as Object),
           ...values,
-        }, { forceQuotes: true }
+        },
+        { forceQuotes: true }
       );
     }
 
@@ -420,6 +421,7 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
       case "settings":
         return (
           <SettingsSection
+            showSource={true}
             currentChart={this.state.currentChart}
             refreshChart={() => this.refreshChart(0)}
             setShowDeleteOverlay={(x: boolean) =>
@@ -554,7 +556,8 @@ export default class ExpandedJobChart extends Component<PropsType, StateType> {
               </Title>
               <InfoWrapper>
                 <LastDeployed>
-                  Run {this.state.jobs.length} times <Dot>•</Dot>Last template update at
+                  Run {this.state.jobs.length} times <Dot>•</Dot>Last template
+                  update at
                   {" " + this.readableDate(chart.info.last_deployed)}
                 </LastDeployed>
               </InfoWrapper>

+ 80 - 57
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -22,6 +22,7 @@ type PropsType = {
   currentChart: ChartType;
   refreshChart: () => void;
   setShowDeleteOverlay: (x: boolean) => void;
+  showSource?: boolean;
 };
 
 type StateType = {
@@ -80,36 +81,76 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       .catch(console.log);
   }
 
-  redeployWithNewImage = (img: string, tag: string) => {
-    this.setState({ saveValuesStatus: "loading" });
-    let { currentCluster, currentProject, setCurrentError } = this.context;
+  renderWebhookSection = () => {
+    if (!this.props.currentChart?.form?.hasSource) {
+      return;
+    }
 
-    // If tag is explicitly declared, parse tag
-    let imgSplits = img.split(":");
-    let parsedTag = null;
-    if (imgSplits.length > 1) {
-      img = imgSplits[0];
-      parsedTag = imgSplits[1];
+    if (true || this.state.webhookToken) {
+      let webhookText = `curl -X POST 'https://dashboard.getporter.dev/api/webhooks/deploy/${this.state.webhookToken}?commit=YOUR_COMMIT_HASH'`;
+      return (
+        <>
+          {this.props.showSource && (
+            <>
+              <Heading>Source Settings</Heading>
+              <Helper>Specify an image tag to use.</Helper>
+              <ImageSelector
+                selectedTag={this.state.selectedTag}
+                selectedImageUrl={this.state.selectedImageUrl}
+                setSelectedImageUrl={(x: string) =>
+                  this.setState({ selectedImageUrl: x })
+                }
+                setSelectedTag={(x: string) =>
+                  this.setState({ selectedTag: x })
+                }
+                forceExpanded={true}
+                disableImageSelect={true}
+              />
+              <Br />
+            </>
+          )}
+          <Heading>Redeploy Webhook</Heading>
+          <Helper>
+            Programmatically deploy by calling this secret webhook.
+          </Helper>
+          <Webhook copiedToClipboard={this.state.highlightCopyButton}>
+            <div>{webhookText}</div>
+            <i
+              className="material-icons"
+              onClick={() => {
+                navigator.clipboard.writeText(webhookText);
+                this.setState({ highlightCopyButton: true });
+              }}
+              onMouseLeave={() => this.setState({ highlightCopyButton: false })}
+            >
+              content_copy
+            </i>
+          </Webhook>
+        </>
+      );
     }
+  };
 
-    let image = {
-      image: {
-        repository: img,
-        tag: parsedTag || tag,
-      },
-    };
+  handleSubmit = () => {
+    let { currentCluster, setCurrentError, currentProject } = this.context;
+    this.setState({ saveValuesStatus: "loading" });
+
+    console.log(this.state.selectedImageUrl);
 
     let values = {};
-    let rawValues = this.props.currentChart.config;
-    for (let key in rawValues) {
-      _.set(values, key, rawValues[key]);
+    if (this.state.selectedTag) {
+      _.set(values, "image.repository", this.state.selectedImageUrl);
+      _.set(values, "image.tag", this.state.selectedTag);
     }
 
     // Weave in preexisting values and convert to yaml
-    let valuesYaml = yaml.dump({
-      ...values,
-      ...image,
-    });
+    let conf = yaml.dump(
+      {
+        ...(this.props.currentChart.config as Object),
+        ...values,
+      },
+      { forceQuotes: true }
+    );
 
     api
       .upgradeChartValues(
@@ -117,7 +158,7 @@ export default class SettingsSection extends Component<PropsType, StateType> {
         {
           namespace: this.props.currentChart.namespace,
           storage: StorageType.Secret,
-          values: valuesYaml,
+          values: conf,
         },
         {
           id: currentProject.id,
@@ -145,41 +186,10 @@ export default class SettingsSection extends Component<PropsType, StateType> {
       });
   };
 
-  renderWebhookSection = () => {
-    if (!this.props.currentChart?.form?.hasSource) {
-      return;
-    }
-
-    if (true || this.state.webhookToken) {
-      let webhookText = `curl -X POST 'https://dashboard.getporter.dev/api/webhooks/deploy/${this.state.webhookToken}?commit=YOUR_COMMIT_HASH'`;
-      return (
-        <>
-          <Heading>Redeploy Webhook</Heading>
-          <Helper>
-            Programmatically deploy by calling this secret webhook.
-          </Helper>
-          <Webhook copiedToClipboard={this.state.highlightCopyButton}>
-            <div>{webhookText}</div>
-            <i
-              className="material-icons"
-              onClick={() => {
-                navigator.clipboard.writeText(webhookText);
-                this.setState({ highlightCopyButton: true });
-              }}
-              onMouseLeave={() => this.setState({ highlightCopyButton: false })}
-            >
-              content_copy
-            </i>
-          </Webhook>
-        </>
-      );
-    }
-  };
-
   render() {
     return (
       <Wrapper>
-        <StyledSettingsSection>
+        <StyledSettingsSection showSource={this.props.showSource}>
           {this.renderWebhookSection()}
           <Heading>Additional Settings</Heading>
           <Button
@@ -189,6 +199,14 @@ export default class SettingsSection extends Component<PropsType, StateType> {
             Delete {this.props.currentChart.name}
           </Button>
         </StyledSettingsSection>
+        {this.props.showSource && (
+          <SaveButton
+            text="Deploy"
+            status={this.state.saveValuesStatus}
+            onClick={this.handleSubmit}
+            makeFlush={true}
+          />
+        )}
       </Wrapper>
     );
   }
@@ -196,6 +214,11 @@ export default class SettingsSection extends Component<PropsType, StateType> {
 
 SettingsSection.contextType = Context;
 
+const Br = styled.div`
+  width: 100%;
+  height: 10px;
+`;
+
 const Button = styled.button`
   height: 35px;
   font-size: 13px;
@@ -280,15 +303,15 @@ const Wrapper = styled.div`
   height: 100%;
 `;
 
-const StyledSettingsSection = styled.div`
+const StyledSettingsSection = styled.div<{ showSource: boolean }>`
   width: 100%;
-  height: calc(100%);
   background: #ffffff11;
   padding: 0 35px;
   padding-bottom: 50px;
   position: relative;
   border-radius: 5px;
   overflow: auto;
+  height: ${(props) => (props.showSource ? "calc(100% - 55px)" : "100%")};
 `;
 
 const Holder = styled.div`

+ 21 - 10
internal/registry/registry.go

@@ -52,6 +52,9 @@ type Image struct {
 
 	// The name of the repository associated with the image.
 	RepositoryName string `json:"repository_name"`
+
+	// When the image was pushed
+	PushedAt *time.Time `json:"pushed_at"`
 }
 
 // ListRepositories lists the repositories for a registry
@@ -479,18 +482,26 @@ func (r *Registry) listECRImages(repoName string, repo repository.Repository) ([
 		return nil, err
 	}
 
+	describeResp, err := svc.DescribeImages(&ecr.DescribeImagesInput{
+		RepositoryName: &repoName,
+		ImageIds:       resp.ImageIds,
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
 	res := make([]*Image, 0)
 
-	for _, img := range resp.ImageIds {
-		if img.ImageTag == nil {
-			continue
+	for _, img := range describeResp.ImageDetails {
+		for _, tag := range img.ImageTags {
+			res = append(res, &Image{
+				Digest:         *img.ImageDigest,
+				Tag:            *tag,
+				RepositoryName: repoName,
+				PushedAt:       img.ImagePushedAt,
+			})
 		}
-
-		res = append(res, &Image{
-			Digest:         *img.ImageDigest,
-			Tag:            *img.ImageTag,
-			RepositoryName: repoName,
-		})
 	}
 
 	return res, nil
@@ -909,4 +920,4 @@ func (r *Registry) getPrivateRegistryDockerConfigFile(
 
 func generateAuthToken(username, password string) string {
 	return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
-}
+}