瀏覽代碼

upgrade chart backend

Alexander Belanger 5 年之前
父節點
當前提交
e718bf73e2

+ 3 - 1
cli/cmd/helm_repo.go

@@ -118,7 +118,9 @@ func listHelmRepoCharts(user *api.AuthCheckResponse, client *api.Client, args []
 	fmt.Fprintf(w, "%s\t%s\n", "NAME", "VERSION")
 
 	for _, chart := range charts {
-		fmt.Fprintf(w, "%s\t%s\n", strings.ToLower(chart.Name), chart.Version)
+		for _, version := range chart.Versions {
+			fmt.Fprintf(w, "%s\t%s\n", strings.ToLower(chart.Name), version)
+		}
 	}
 
 	w.Flush()

文件差異過大導致無法顯示
+ 10426 - 1
dashboard/package-lock.json


+ 2 - 0
dashboard/package.json

@@ -41,6 +41,7 @@
     "react-dom": "^16.13.1",
     "react-modal": "^3.11.2",
     "react-router-dom": "^5.2.0",
+    "semver": "^7.3.5",
     "styled-components": "^5.2.0"
   },
   "scripts": {
@@ -62,6 +63,7 @@
     "@types/react-modal": "^3.10.6",
     "@types/react-router": "^5.1.8",
     "@types/react-router-dom": "^5.1.5",
+    "@types/semver": "^7.3.5",
     "@types/styled-components": "^5.1.3",
     "file-loader": "^6.1.0",
     "html-webpack-plugin": "^4.5.0",

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

@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import styled from "styled-components";
 import Modal from "../../main/home/modals/Modal";
 import LoadEnvGroupModal from "../../main/home/modals/LoadEnvGroupModal";
-import EnvEditorModal from "../../main/home/modals/EnvEditorModal";
+import EnvEditorModal from "../../main/home/modals/EnvEditorModal"
 
 import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";

+ 7 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -26,6 +26,7 @@ import MetricsSection from "./metrics/MetricsSection";
 import ListSection from "./ListSection";
 import StatusSection from "./status/StatusSection";
 import SettingsSection from "./SettingsSection";
+import ChartList from "../chart/ChartList";
 
 type PropsType = {
   namespace: string;
@@ -43,6 +44,7 @@ type StateType = {
   components: ResourceType[];
   podSelectors: string[];
   isPreview: boolean;
+  isUpdatingChart: boolean;
   devOpsMode: boolean;
   tabOptions: any[];
   tabContents: any;
@@ -64,6 +66,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     components: [] as ResourceType[],
     podSelectors: [] as string[],
     isPreview: false,
+    isUpdatingChart: false,
     devOpsMode: localStorage.getItem("devOpsMode") === "true",
     tabOptions: [] as any[],
     tabContents: [] as any,
@@ -415,9 +418,9 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     // Settings tab is always last
     tabOptions.push({ label: "Settings", value: "settings" });
 
-    // Filter tabs if previewing an old revision
-    if (this.state.isPreview) {
-      let liveTabs = ["status", "settings", "deploy"];
+    // Filter tabs if previewing an old revision or updating the chart version
+    if (this.state.isPreview || this.state.isUpdatingChart) {
+      let liveTabs = ["status", "settings", "deploy", "metrics"];
       tabOptions = tabOptions.filter(
         (tab: any) => !liveTabs.includes(tab.value)
       );
@@ -712,6 +715,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
                 this.setState({ forceRefreshRevisions: false })
               }
               status={status}
+              shouldUpdate={chart.latest_version && chart.latest_version !== chart.chart.metadata.version}
             />
           </HeaderWrapper>
 

+ 22 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/RevisionSection.tsx

@@ -17,6 +17,7 @@ type PropsType = {
   forceRefreshRevisions: boolean;
   refreshRevisionsOff: () => void;
   status: string;
+  shouldUpdate: boolean;
 };
 
 type StateType = {
@@ -159,6 +160,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
           <Td>{revision.version}</Td>
           <Td>{this.readableDate(revision.info.last_deployed)}</Td>
           <Td>{this.renderStatus(revision)}</Td>
+          <Td>v{revision.chart.metadata.version}</Td>
           <Td>
             <RollbackButton
               disabled={isCurrent}
@@ -184,6 +186,7 @@ export default class RevisionSection extends Component<PropsType, StateType> {
                 <Th>Revision No.</Th>
                 <Th>Timestamp</Th>
                 <Th>Status</Th>
+                <Th>Template Version</Th>
                 <Th>Rollback</Th>
               </Tr>
               {this.renderRevisionList()}
@@ -220,6 +223,10 @@ export default class RevisionSection extends Component<PropsType, StateType> {
             : `Previewing Revision (Not Deployed)`}{" "}
           - <Revision>No. {this.props.chart.version}</Revision>
           <i className="material-icons">arrow_drop_down</i>
+          <RevisionUpdateMessage>
+            <i className="material-icons">notification_important</i>
+            {!this.props.shouldUpdate ? `Update available` : ""}
+          </RevisionUpdateMessage>
         </RevisionHeader>
 
         <RevisionList>{this.renderExpanded()}</RevisionList>
@@ -389,3 +396,18 @@ const StyledRevisionSection = styled.div`
     }
   }
 `;
+
+const RevisionUpdateMessage = styled.div`
+  position: absolute; 
+  right: 40px; 
+  color: white;
+  display: flex;
+  align-items: center;
+
+  > i {
+    margin-right: 6px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+  }
+`

+ 33 - 6
dashboard/src/main/home/launch/Launch.tsx

@@ -11,6 +11,8 @@ import Loading from "components/Loading";
 
 import hardcodedNames from "./hardcodedNameDict";
 import { Link } from "react-router-dom";
+import semver from "semver";
+import { version } from "html-webpack-plugin";
 
 const tabOptions = [
   { label: "New Application", value: "docker" },
@@ -42,8 +44,21 @@ export default class Templates extends Component<PropsType, StateType> {
     api
       .getAddonTemplates("<token>", {}, {})
       .then((res) => {
-        this.setState({ addonTemplates: res.data, error: false }, () => {
+        let sortedVersionData = res.data.map((template : any) => {
+          let versions = template.versions.reverse()
+
+          versions = template.versions.sort(semver.rcompare)
+
+          return {
+            ...template,
+            versions,
+            currentVersion: versions[0],
+          }          
+        })
+
+        this.setState({ addonTemplates: sortedVersionData, error: false }, () => {
           this.state.addonTemplates.sort((a, b) => (a.name > b.name ? 1 : -1));
+
           this.setState({
             loading: false,
           });
@@ -60,7 +75,19 @@ export default class Templates extends Component<PropsType, StateType> {
         {}
       )
       .then((res) => {
-        this.setState({ applicationTemplates: res.data, error: false }, () => {
+        let sortedVersionData = res.data.map((template : any) => {
+          let versions = template.versions.reverse()
+
+          versions = template.versions.sort(semver.rcompare)
+
+          return {
+            ...template,
+            versions,
+            currentVersion: versions[0],
+          }          
+        })
+
+        this.setState({ applicationTemplates: sortedVersionData, error: false }, () => {
           let preferredOrder = ["web", "worker", "job"];
           this.state.applicationTemplates.sort((a, b) => {
             return (
@@ -198,9 +225,9 @@ export default class Templates extends Component<PropsType, StateType> {
         <ExpandedTemplate
           currentTab={this.state.currentTab}
           currentTemplate={this.state.currentTemplate}
-          setCurrentTemplate={(currentTemplate: PorterTemplate) =>
+          setCurrentTemplate={(currentTemplate: PorterTemplate) => {
             this.setState({ currentTemplate })
-          }
+          }}
           skipDescription={false}
         />
       );
@@ -214,9 +241,9 @@ export default class Templates extends Component<PropsType, StateType> {
         <ExpandedTemplate
           currentTab={this.state.currentTab}
           currentTemplate={this.state.currentTemplate}
-          setCurrentTemplate={(currentTemplate: PorterTemplate) =>
+          setCurrentTemplate={(currentTemplate: PorterTemplate) => {
             this.setState({ currentTemplate })
-          }
+          }}
         />
       );
     }

+ 12 - 2
dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx

@@ -7,6 +7,7 @@ import api from "shared/api";
 import TemplateInfo from "./TemplateInfo";
 import LaunchTemplate from "./LaunchTemplate";
 import Loading from "components/Loading";
+import { template } from "lodash";
 
 type PropsType = {
   currentTemplate: PorterTemplate;
@@ -50,7 +51,7 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
     api
       .getTemplateInfo("<token>", params, {
         name: this.props.currentTemplate.name.toLowerCase().trim(),
-        version: "latest",
+        version: this.props.currentTemplate.currentVersion,
       })
       .then((res) => {
         let { form, values, markdown, metadata } = res.data;
@@ -68,7 +69,8 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
   };
 
   componentDidUpdate = (prevProps: PropsType) => {
-    if (prevProps.currentTemplate !== this.props.currentTemplate) {
+    if (prevProps.currentTemplate.name !== this.props.currentTemplate.name ||
+      prevProps.currentTemplate.currentVersion !== this.props.currentTemplate.currentVersion) {
       this.fetchTemplateInfo();
     }
   };
@@ -100,6 +102,14 @@ export default class ExpandedTemplate extends Component<PropsType, StateType> {
           currentTab={this.props.currentTab}
           currentTemplate={this.props.currentTemplate}
           setCurrentTemplate={this.props.setCurrentTemplate}
+          setCurrentVersion={(version) => {
+            let template = {
+              ...this.props.currentTemplate,
+              currentVersion: version,
+            }
+
+            this.props.setCurrentTemplate(template)
+          }}
           launchTemplate={() => this.setState({ showLaunchTemplate: true })}
           markdown={this.state.markdown}
           keywords={this.state.keywords}

+ 40 - 8
dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx

@@ -7,6 +7,7 @@ import { Context } from "shared/Context";
 
 import { PorterTemplate } from "shared/types";
 import Helper from "components/values-form/Helper";
+import Selector from "components/Selector";
 
 import hardcodedNames from "../hardcodedNameDict";
 
@@ -14,14 +15,21 @@ type PropsType = {
   currentTemplate: any;
   currentTab: string;
   setCurrentTemplate: (x: PorterTemplate) => void;
+  setCurrentVersion: (x: string) => void;
   launchTemplate: () => void;
   markdown: string | null;
   keywords: string[];
 };
 
-type StateType = {};
+type StateType = {
+  currentVersion: string;
+};
 
 export default class TemplateInfo extends Component<PropsType, StateType> {
+  state = {
+    currentVersion: this.props.currentTemplate.currentVersion,
+  }
+
   renderIcon = (icon: string) => {
     if (icon) {
       return <Icon src={icon} />;
@@ -122,6 +130,13 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
       name = hardcodedNames[name];
     }
 
+    let versionOptions = this.props.currentTemplate.versions.map((version: string) => {
+      return {
+        value: version,
+        label: "v" + version,
+      }
+    })
+
     return (
       <StyledExpandedTemplate>
         <TitleSection>
@@ -137,13 +152,24 @@ export default class TemplateInfo extends Component<PropsType, StateType> {
               : this.renderIcon(currentTemplate.icon)}
             <Title>{name}</Title>
           </Flex>
-          <Button
-            isDisabled={!currentCluster}
-            onClick={!currentCluster ? null : this.props.launchTemplate}
-          >
-            <img src={rocket} />
-            Launch Template
-          </Button>
+          <StyledVersionSelector>
+            <Selector
+                activeValue={this.props.currentTemplate.currentVersion}
+                setActiveValue={(version) => this.props.setCurrentVersion(version)}
+                options={versionOptions}
+                dropdownLabel="Version"
+                width="150px"
+                dropdownWidth="230px"
+                closeOverlay={true}
+              />
+            <Button
+              isDisabled={!currentCluster}
+              onClick={!currentCluster ? null : this.props.launchTemplate}
+            >
+              <img src={rocket} />
+              Launch Template
+            </Button>
+          </StyledVersionSelector>
         </TitleSection>
         <Helper>{description}</Helper>
         {this.renderTagSection()}
@@ -258,6 +284,7 @@ const Button = styled.div`
   display: flex;
   flex-direction: row;
   align-items: center;
+  margin-left: 10px; 
 
   > img {
     width: 16px;
@@ -309,3 +336,8 @@ const TitleSection = styled.div`
 const StyledExpandedTemplate = styled.div`
   width: 100%;
 `;
+
+const StyledVersionSelector = styled.div`
+  display: flex;
+  font-size: 13px;
+`;

+ 3 - 1
dashboard/src/shared/types.tsx

@@ -34,6 +34,7 @@ export interface ChartType {
   config: any;
   version: number;
   namespace: string;
+  latest_version: string;
 }
 
 export interface ResourceType {
@@ -72,7 +73,8 @@ export enum StorageType {
 // PorterTemplate represents a bundled Porter template
 export interface PorterTemplate {
   name: string;
-  version: string;
+  versions: string[];
+  currentVersion: string;
   description: string;
   icon: string;
 }

+ 12 - 0
internal/helm/agent.go

@@ -60,6 +60,10 @@ type UpgradeReleaseConfig struct {
 	Cluster    *models.Cluster
 	Repo       repository.Repository
 	Registries []*models.Registry
+
+	// The version of the chart to use (optional). If not set, the chart from the previous
+	// release is re-used
+	// ChartVersion *string
 }
 
 // UpgradeRelease upgrades a specific release with new values.yaml
@@ -91,6 +95,14 @@ func (a *Agent) UpgradeReleaseByValues(
 		return nil, fmt.Errorf("Could not get release to be upgraded: %v", err)
 	}
 
+	// var ch *chart.Chart
+
+	// if conf.ChartVersion == nil {
+	// 	ch = rel.Chart
+	// } else {
+	// 	// get the chart version from the repo
+	// }
+
 	ch := rel.Chart
 
 	cmd := action.NewUpgrade(a.ActionConfig)

+ 38 - 3
internal/helm/loader/loader.go

@@ -17,16 +17,24 @@ import (
 
 // RepoIndexToPorterChartList converts an index file to a list of porter charts
 func RepoIndexToPorterChartList(index *repo.IndexFile) []*models.PorterChartList {
+	// sort the entries before parsing
+	index.SortEntries()
+
 	porterCharts := make([]*models.PorterChartList, 0)
 
-	for _, entry := range index.Entries {
-		indexChart := entry[0]
+	for _, entryVersions := range index.Entries {
+		indexChart := entryVersions[0]
+		versions := make([]string, 0)
+
+		for _, entryVersion := range entryVersions {
+			versions = append(versions, entryVersion.Version)
+		}
 
 		porterChart := &models.PorterChartList{
 			Name:        indexChart.Name,
-			Version:     indexChart.Version,
 			Description: indexChart.Description,
 			Icon:        indexChart.Icon,
+			Versions:    versions,
 		}
 
 		porterCharts = append(porterCharts, porterChart)
@@ -35,6 +43,33 @@ func RepoIndexToPorterChartList(index *repo.IndexFile) []*models.PorterChartList
 	return porterCharts
 }
 
+// FindPorterChartInIndexList finds a chart by name given an index file and returns it
+func FindPorterChartInIndexList(index *repo.IndexFile, name string) *models.PorterChartList {
+	// sort the entries before parsing
+	index.SortEntries()
+
+	for _, entryVersions := range index.Entries {
+		indexChart := entryVersions[0]
+
+		if indexChart.Name == name {
+			versions := make([]string, 0)
+
+			for _, entryVersion := range entryVersions {
+				versions = append(versions, entryVersion.Version)
+			}
+
+			return &models.PorterChartList{
+				Name:        indexChart.Name,
+				Description: indexChart.Description,
+				Icon:        indexChart.Icon,
+				Versions:    versions,
+			}
+		}
+	}
+
+	return nil
+}
+
 // BasicAuthClient is just a username/password to set on requests
 type BasicAuthClient struct {
 	Username string

+ 4 - 4
internal/models/templates.go

@@ -4,10 +4,10 @@ import "helm.sh/helm/v3/pkg/chart"
 
 // PorterChartList is how a chart gets displayed when listed
 type PorterChartList struct {
-	Name        string `json:"name"`
-	Version     string `json:"version"`
-	Description string `json:"description"`
-	Icon        string `json:"icon"`
+	Name        string   `json:"name"`
+	Versions    []string `json:"versions"`
+	Description string   `json:"description"`
+	Icon        string   `json:"icon"`
 }
 
 // PorterChartRead is a chart with detailed information and a form for reading

+ 19 - 3
server/api/release_handler.go

@@ -19,6 +19,7 @@ import (
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm/grapher"
+	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/repository"
@@ -76,10 +77,13 @@ func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
 // PorterRelease is a helm release with a form attached
 type PorterRelease struct {
 	*release.Release
-	Form       *models.FormYAML `json:"form"`
-	HasMetrics bool             `json:"has_metrics"`
+	Form          *models.FormYAML `json:"form"`
+	HasMetrics    bool             `json:"has_metrics"`
+	LatestVersion string           `json:"latest_version"`
 }
 
+var porterApplications = map[string]string{"web": "", "job": "", "worker": ""}
+
 // HandleGetRelease retrieves a single release based on a name and revision
 func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 	name := chi.URLParam(r, "name")
@@ -157,7 +161,7 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 		HelmRelease:   release,
 	}
 
-	res := &PorterRelease{release, nil, false}
+	res := &PorterRelease{release, nil, false, ""}
 
 	for _, file := range release.Chart.Files {
 		if strings.Contains(file.Name, "form.yaml") {
@@ -194,6 +198,18 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 
 	res.HasMetrics = found
 
+	// detect if Porter application chart and attempt to get the latest version
+	// from chart repo
+	if _, found := porterApplications[res.Chart.Metadata.Name]; found {
+		repoIndex, err := loader.LoadRepoIndexPublic(app.ServerConf.DefaultHelmRepoURL)
+
+		if err == nil {
+			porterChart := loader.FindPorterChartInIndexList(repoIndex, res.Chart.Metadata.Name)
+
+			res.LatestVersion = porterChart.Versions[0]
+		}
+	}
+
 	if err := json.NewEncoder(w).Encode(res); err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return

部分文件因文件數量過多而無法顯示