Преглед изворни кода

Merge branch 'master' into 0.8.0-more-slack-options

Ivan Galakhov пре 4 година
родитељ
комит
8f429e00d0
32 измењених фајлова са 779 додато и 154 уклоњено
  1. 8 9
      cli/cmd/api/github_action.go
  2. 9 0
      cli/cmd/create.go
  3. 8 1
      cli/cmd/deploy/create.go
  4. 4 2
      cli/cmd/docker/agent.go
  5. 1 1
      cli/cmd/version.go
  6. 5 2
      dashboard/src/components/porter-form/PorterFormContextProvider.tsx
  7. 3 0
      dashboard/src/components/porter-form/PorterFormWrapper.tsx
  8. 5 1
      dashboard/src/components/porter-form/field-components/Input.tsx
  9. 5 1
      dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx
  10. 539 0
      dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx
  11. 1 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx
  12. 18 3
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts
  13. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx
  14. 10 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/types.ts
  15. 2 4
      dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx
  16. 2 2
      dashboard/src/shared/api.tsx
  17. 21 9
      docs/guides/running-porter-locally.md
  18. 10 10
      internal/forms/git_action.go
  19. 10 26
      internal/integrations/ci/actions/actions.go
  20. 12 29
      internal/integrations/ci/actions/steps.go
  21. 1 1
      internal/kubernetes/config.go
  22. 20 8
      internal/kubernetes/prometheus/metrics.go
  23. 2 0
      internal/models/gitrepo.go
  24. 9 5
      internal/models/integrations/gcp.go
  25. 16 7
      internal/registry/registry.go
  26. 5 1
      server/api/api.go
  27. 0 2
      server/api/deploy_handler.go
  28. 6 3
      server/api/git_action_handler.go
  29. 8 3
      server/api/git_repo_handler.go
  30. 8 7
      server/api/integration_handler.go
  31. 28 14
      server/api/release_handler.go
  32. 1 1
      server/api/user_handler.go

+ 8 - 9
cli/cmd/api/github_action.go

@@ -11,15 +11,14 @@ import (
 // CreateGithubActionRequest represents the accepted fields for creating
 // CreateGithubActionRequest represents the accepted fields for creating
 // a Github action
 // a Github action
 type CreateGithubActionRequest struct {
 type CreateGithubActionRequest struct {
-	ReleaseID      uint              `json:"release_id" form:"required"`
-	GitRepo        string            `json:"git_repo" form:"required"`
-	GitBranch      string            `json:"git_branch"`
-	ImageRepoURI   string            `json:"image_repo_uri" form:"required"`
-	DockerfilePath string            `json:"dockerfile_path"`
-	FolderPath     string            `json:"folder_path"`
-	GitRepoID      uint              `json:"git_repo_id" form:"required"`
-	BuildEnv       map[string]string `json:"env"`
-	RegistryID     uint              `json:"registry_id"`
+	ReleaseID      uint   `json:"release_id" form:"required"`
+	GitRepo        string `json:"git_repo" form:"required"`
+	GitBranch      string `json:"git_branch"`
+	ImageRepoURI   string `json:"image_repo_uri" form:"required"`
+	DockerfilePath string `json:"dockerfile_path"`
+	FolderPath     string `json:"folder_path"`
+	GitRepoID      uint   `json:"git_repo_id" form:"required"`
+	RegistryID     uint   `json:"registry_id"`
 }
 }
 
 
 // CreateGithubAction creates a Github action with basic authentication
 // CreateGithubAction creates a Github action with basic authentication

+ 9 - 0
cli/cmd/create.go

@@ -74,6 +74,7 @@ var name string
 var values string
 var values string
 var source string
 var source string
 var image string
 var image string
+var registryURL string
 
 
 func init() {
 func init() {
 	rootCmd.AddCommand(createCmd)
 	rootCmd.AddCommand(createCmd)
@@ -137,6 +138,13 @@ func init() {
 		"",
 		"",
 		"if the source is \"registry\", the image to use, in repository:tag format",
 		"if the source is \"registry\", the image to use, in repository:tag format",
 	)
 	)
+
+	createCmd.PersistentFlags().StringVar(
+		&registryURL,
+		"registry-url",
+		"",
+		"the registry URL to use (must exist in \"porter registries list\")",
+	)
 }
 }
 
 
 var supportedKinds = map[string]string{"web": "", "job": "", "worker": ""}
 var supportedKinds = map[string]string{"web": "", "job": "", "worker": ""}
@@ -183,6 +191,7 @@ func createFull(resp *api.AuthCheckResponse, client *api.Client, args []string)
 			},
 			},
 			Kind:        args[0],
 			Kind:        args[0],
 			ReleaseName: name,
 			ReleaseName: name,
+			RegistryURL: registryURL,
 		},
 		},
 	}
 	}
 
 

+ 8 - 1
cli/cmd/deploy/create.go

@@ -25,6 +25,7 @@ type CreateOpts struct {
 
 
 	Kind        string
 	Kind        string
 	ReleaseName string
 	ReleaseName string
+	RegistryURL string
 }
 }
 
 
 // GithubOpts are the options for linking a Github source to the app
 // GithubOpts are the options for linking a Github source to the app
@@ -367,7 +368,13 @@ func (c *CreateAgent) GetImageRepoURL(name, namespace string) (uint, string, err
 	var regID uint
 	var regID uint
 
 
 	for _, reg := range registries {
 	for _, reg := range registries {
-		if reg.URL != "" {
+		if c.CreateOpts.RegistryURL != "" {
+			if c.CreateOpts.RegistryURL == reg.URL {
+				regID = reg.ID
+				imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
+				break
+			}
+		} else if reg.URL != "" {
 			regID = reg.ID
 			regID = reg.ID
 			imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
 			imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
 			break
 			break

+ 4 - 2
cli/cmd/docker/agent.go

@@ -200,12 +200,14 @@ func (a *Agent) PushImage(image string) error {
 		opts,
 		opts,
 	)
 	)
 
 
+	if out != nil {
+		defer out.Close()
+	}
+
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	defer out.Close()
-
 	termFd, isTerm := term.GetFdInfo(os.Stderr)
 	termFd, isTerm := term.GetFdInfo(os.Stderr)
 
 
 	return jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil)
 	return jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil)

+ 1 - 1
cli/cmd/version.go

@@ -7,7 +7,7 @@ import (
 )
 )
 
 
 // Version will be linked by an ldflag during build
 // Version will be linked by an ldflag during build
-var Version string = "v0.5.0"
+var Version string = "v0.8.0"
 
 
 var versionCmd = &cobra.Command{
 var versionCmd = &cobra.Command{
 	Use:     "version",
 	Use:     "version",

+ 5 - 2
dashboard/src/components/porter-form/PorterFormContextProvider.tsx

@@ -5,7 +5,7 @@ import {
   PorterFormData,
   PorterFormData,
   PorterFormState,
   PorterFormState,
   PorterFormValidationInfo,
   PorterFormValidationInfo,
-  PorterFormVariableList
+  PorterFormVariableList,
 } from "./types";
 } from "./types";
 import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
 import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
 import { getFinalVariablesForStringInput } from "./field-components/Input";
@@ -20,6 +20,7 @@ interface Props {
   onSubmit: (vars: PorterFormVariableList) => void;
   onSubmit: (vars: PorterFormVariableList) => void;
   initialVariables?: PorterFormVariableList;
   initialVariables?: PorterFormVariableList;
   overrideVariables?: PorterFormVariableList;
   overrideVariables?: PorterFormVariableList;
+  includeHiddenFields?: boolean;
   isReadOnly?: boolean;
   isReadOnly?: boolean;
   doDebug?: boolean;
   doDebug?: boolean;
 }
 }
@@ -402,7 +403,9 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
       select: getFinalVariablesForSelect,
       select: getFinalVariablesForSelect,
     };
     };
 
 
-    const data = props.rawFormData.includeHiddenFields
+    const data = props.includeHiddenFields
+      ? restructureToNewFields(props.rawFormData)
+      : props.rawFormData.includeHiddenFields
       ? restructureToNewFields(props.rawFormData)
       ? restructureToNewFields(props.rawFormData)
       : formData;
       : formData;
 
 

+ 3 - 0
dashboard/src/components/porter-form/PorterFormWrapper.tsx

@@ -19,6 +19,7 @@ type PropsType = {
   saveValuesStatus?: string;
   saveValuesStatus?: string;
   showStateDebugger?: boolean;
   showStateDebugger?: boolean;
   isLaunch?: boolean;
   isLaunch?: boolean;
+  includeHiddenFields?: boolean;
 };
 };
 
 
 const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
 const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
@@ -36,6 +37,7 @@ const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
   saveValuesStatus,
   saveValuesStatus,
   showStateDebugger,
   showStateDebugger,
   isLaunch,
   isLaunch,
+  includeHiddenFields,
 }) => {
 }) => {
   const hashCode = (s: string) => {
   const hashCode = (s: string) => {
     return s?.split("").reduce(function (a, b) {
     return s?.split("").reduce(function (a, b) {
@@ -72,6 +74,7 @@ const PorterFormWrapper: React.FunctionComponent<PropsType> = ({
         overrideVariables={valuesToOverride}
         overrideVariables={valuesToOverride}
         isReadOnly={isReadOnly}
         isReadOnly={isReadOnly}
         onSubmit={onSubmit}
         onSubmit={onSubmit}
+        includeHiddenFields={includeHiddenFields}
       >
       >
         <PorterForm
         <PorterForm
           showStateDebugger={showStateDebugger}
           showStateDebugger={showStateDebugger}

+ 5 - 1
dashboard/src/components/porter-form/field-components/Input.tsx

@@ -1,7 +1,11 @@
 import React from "react";
 import React from "react";
 import InputRow from "../../form-components/InputRow";
 import InputRow from "../../form-components/InputRow";
 import useFormField from "../hooks/useFormField";
 import useFormField from "../hooks/useFormField";
-import { GetFinalVariablesFunction, InputField, StringInputFieldState } from "../types";
+import {
+  GetFinalVariablesFunction,
+  InputField,
+  StringInputFieldState,
+} from "../types";
 
 
 const clipOffUnit = (unit: string, x: string) => {
 const clipOffUnit = (unit: string, x: string) => {
   if (typeof x === "string" && unit) {
   if (typeof x === "string" && unit) {

+ 5 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/Dashboard.tsx

@@ -10,14 +10,16 @@ import NodeList from "./NodeList";
 import { NamespaceList } from "./NamespaceList";
 import { NamespaceList } from "./NamespaceList";
 import ClusterSettings from "./ClusterSettings";
 import ClusterSettings from "./ClusterSettings";
 import useAuth from "shared/auth/useAuth";
 import useAuth from "shared/auth/useAuth";
+import Metrics from "./Metrics";
 
 
-type TabEnum = "nodes" | "settings" | "namespaces";
+type TabEnum = "nodes" | "settings" | "namespaces" | "metrics";
 
 
 const tabOptions: {
 const tabOptions: {
   label: string;
   label: string;
   value: TabEnum;
   value: TabEnum;
 }[] = [
 }[] = [
   { label: "Nodes", value: "nodes" },
   { label: "Nodes", value: "nodes" },
+  { label: "Metrics", value: "metrics" },
   { label: "Namespaces", value: "namespaces" },
   { label: "Namespaces", value: "namespaces" },
   { label: "Settings", value: "settings" },
   { label: "Settings", value: "settings" },
 ];
 ];
@@ -32,6 +34,8 @@ export const Dashboard: React.FunctionComponent = () => {
     switch (currentTab) {
     switch (currentTab) {
       case "settings":
       case "settings":
         return <ClusterSettings />;
         return <ClusterSettings />;
+      case "metrics":
+        return <Metrics />;
       case "namespaces":
       case "namespaces":
         return <NamespaceList />;
         return <NamespaceList />;
       case "nodes":
       case "nodes":

+ 539 - 0
dashboard/src/main/home/cluster-dashboard/dashboard/Metrics.tsx

@@ -0,0 +1,539 @@
+import React, { useContext, useState, useEffect } from "react";
+import { Context } from "../../../../shared/Context";
+import api from "../../../../shared/api";
+import styled from "styled-components";
+import Loading from "../../../../components/Loading";
+import settings from "../../../../assets/settings.svg";
+import TabSelector from "../../../../components/TabSelector";
+import CheckboxRow from "../../../../components/form-components/CheckboxRow";
+import ParentSize from "@visx/responsive/lib/components/ParentSize";
+import AreaChart from "../expanded-chart/metrics/AreaChart";
+import {
+  AvailableMetrics,
+  NormalizedMetricsData,
+} from "../expanded-chart/metrics/types";
+import SelectRow from "../../../../components/form-components/SelectRow";
+import { MetricNormalizer } from "../expanded-chart/metrics/MetricNormalizer";
+import {
+  resolutions,
+  secondsBeforeNow,
+} from "../expanded-chart/metrics/MetricsSection";
+
+const Metrics: React.FC = () => {
+  const { currentProject, currentCluster, setCurrentError } = useContext(
+    Context
+  );
+  const [loading, setLoading] = useState(true);
+  const [detected, setDetected] = useState(false);
+  const [metricsOptions, setMetricsOptions] = useState([]);
+  const [dropdownExpanded, setDropdownExpanded] = useState(false);
+  const [ingressOptions, setIngressOptions] = useState([]);
+  const [selectedIngress, setSelectedIngress] = useState(null);
+  const [selectedRange, setSelectedRange] = useState("1H");
+  const [selectedMetric, setSelectedMetric] = useState("nginx:errors");
+  const [selectedMetricLabel, setSelectedMetricLabel] = useState(
+    "5XX Error Percentage"
+  );
+  const [selectedPercentile, setSelectedPercentile] = useState("0.99");
+  const [data, setData] = useState<NormalizedMetricsData[]>([]);
+  const [showMetricsSettings, setShowMetricsSettings] = useState(false);
+  const [isLoading, setIsLoading] = useState(0);
+  const [hpaData, setHpaData] = useState([]);
+
+  useEffect(() => {
+    if (selectedMetric && selectedRange && selectedIngress) {
+      getMetrics();
+    }
+  }, [selectedMetric, selectedRange, selectedIngress, selectedPercentile]);
+
+  useEffect(() => {
+    Promise.all([
+      api.getCluster(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      ),
+      api.getPrometheusIsInstalled(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+        },
+        {
+          id: currentProject.id,
+        }
+      ),
+    ])
+      .then(() => {
+        setDetected(true);
+        setIsLoading((prev) => prev + 1);
+
+        api
+          .getNGINXIngresses(
+            "<token>",
+            {
+              cluster_id: currentCluster.id,
+            },
+            {
+              id: currentProject.id,
+            }
+          )
+          .then((res) => {
+            const ingressOptions = res.data.map((ingress: any) => ({
+              value: ingress,
+              label: ingress.name,
+            }));
+            setIngressOptions(ingressOptions);
+            setSelectedIngress(ingressOptions[0]?.value);
+            setMetricsOptions([
+              ...metricsOptions,
+              {
+                value: "nginx:errors",
+                label: "5XX Error Percentage",
+              },
+              {
+                value: "nginx:latency",
+                label: "Request Latency (s)",
+              },
+              {
+                value: "nginx:latency-histogram",
+                label: "Percentile Response Times (s)",
+              },
+            ]);
+            setLoading(false);
+          })
+          .catch((err) => {
+            setCurrentError(JSON.stringify(err));
+          })
+          .finally(() => {
+            setIsLoading((prev) => prev - 1);
+          });
+      })
+      .catch(() => {
+        setDetected(false);
+        setLoading(false);
+      });
+  }, []);
+
+  const renderMetricsSettings = () => {
+    if (showMetricsSettings) {
+      return (
+        <>
+          <DropdownOverlay onClick={() => setShowMetricsSettings(false)} />
+          <DropdownAlt dropdownWidth="330px" dropdownMaxHeight="300px">
+            <Label>Additional Settings</Label>
+            <SelectRow
+              label="Target Ingress"
+              value={selectedIngress}
+              setActiveValue={(x: any) => setSelectedIngress(x)}
+              options={ingressOptions}
+              width="100%"
+            />
+            {selectedMetric == "nginx:latency-histogram" && (
+              <SelectRow
+                label="Percentile"
+                value={selectedPercentile}
+                setActiveValue={(x) => {
+                  setSelectedPercentile(x);
+                }}
+                options={[
+                  {
+                    label: "99",
+                    value: "0.99",
+                  },
+                  {
+                    label: "95",
+                    value: "0.95",
+                  },
+                  {
+                    label: "50",
+                    value: "0.5",
+                  },
+                ]}
+                width="100%"
+              />
+            )}
+          </DropdownAlt>
+        </>
+      );
+    }
+  };
+
+  const renderDropdown = () => {
+    if (dropdownExpanded) {
+      return (
+        <>
+          <DropdownOverlay onClick={() => setDropdownExpanded(false)} />
+          <Dropdown
+            dropdownWidth="230px"
+            dropdownMaxHeight="200px"
+            onClick={() => setDropdownExpanded(false)}
+          >
+            {renderOptionList()}
+          </Dropdown>
+        </>
+      );
+    }
+  };
+
+  const renderOptionList = () => {
+    return metricsOptions.map(
+      (option: { value: string; label: string }, i: number) => {
+        return (
+          <Option
+            key={i}
+            selected={option.value === selectedMetric}
+            onClick={() => {
+              setSelectedMetric(option.value);
+              setSelectedMetricLabel(option.label);
+            }}
+            lastItem={i === metricsOptions.length - 1}
+          >
+            {option.label}
+          </Option>
+        );
+      }
+    );
+  };
+
+  const getMetrics = async () => {
+    try {
+      let shouldsum = true;
+      let namespace = "default";
+
+      // calculate start and end range
+      const d = new Date();
+      const end = Math.round(d.getTime() / 1000);
+      const start = end - secondsBeforeNow[selectedRange];
+
+      let podNames = [] as string[];
+
+      podNames = [selectedIngress?.name];
+
+      setIsLoading((prev) => prev + 1);
+      setData([]);
+
+      const res = await api.getMetrics(
+        "<token>",
+        {
+          cluster_id: currentCluster.id,
+          metric: selectedMetric,
+          shouldsum: false,
+          kind: "Ingress",
+          namespace: selectedIngress?.namespace || "default",
+          percentile:
+            selectedMetric == "nginx:latency-histogram"
+              ? parseFloat(selectedPercentile)
+              : undefined,
+          startrange: start,
+          endrange: end,
+          resolution: resolutions[selectedRange],
+          pods: podNames,
+          name: selectedIngress?.name,
+        },
+        {
+          id: currentProject.id,
+        }
+      );
+
+      setHpaData([]);
+
+      const metrics = new MetricNormalizer(
+        res.data,
+        selectedMetric as AvailableMetrics
+      );
+
+      // transform the metrics to expected form
+      setData(metrics.getParsedData());
+    } catch (error) {
+      setCurrentError(JSON.stringify(error));
+    } finally {
+      setIsLoading((prev) => prev - 1);
+    }
+  };
+
+  return loading ? (
+    <LoadingWrapper>
+      <Loading />
+    </LoadingWrapper>
+  ) : !detected ? (
+    <p>
+      This message displays when either there's no ingress controller or nginx
+      is not installed
+    </p>
+  ) : (
+    <StyledMetricsSection>
+      <Header>
+        <Flex>
+          <MetricSelector
+            onClick={() => setDropdownExpanded(!dropdownExpanded)}
+          >
+            <MetricsLabel>{selectedMetricLabel}</MetricsLabel>
+            <i className="material-icons">arrow_drop_down</i>
+            {renderDropdown()}
+          </MetricSelector>
+          <Relative>
+            <IconWrapper onClick={() => setShowMetricsSettings(true)}>
+              <SettingsIcon src={settings} />
+            </IconWrapper>
+            {renderMetricsSettings()}
+          </Relative>
+
+          <Highlight color={"#7d7d81"} onClick={getMetrics}>
+            <i className="material-icons">autorenew</i>
+          </Highlight>
+        </Flex>
+        <RangeWrapper>
+          <TabSelector
+            noBuffer={true}
+            options={[
+              { value: "1H", label: "1H" },
+              { value: "6H", label: "6H" },
+              { value: "1D", label: "1D" },
+              { value: "1M", label: "1M" },
+            ]}
+            currentTab={selectedRange}
+            setCurrentTab={(x: string) => setSelectedRange(x)}
+          />
+        </RangeWrapper>
+      </Header>
+      {isLoading > 0 && <Loading />}
+      {data.length === 0 && isLoading === 0 && (
+        <Message>
+          No data available yet.
+          <Highlight color={"#8590ff"} onClick={getMetrics}>
+            <i className="material-icons">autorenew</i>
+            Refresh
+          </Highlight>
+        </Message>
+      )}
+      {data.length > 0 && isLoading === 0 && (
+        <>
+          <ParentSize>
+            {({ width, height }) => (
+              <AreaChart
+                dataKey={selectedMetricLabel}
+                data={data}
+                hpaData={hpaData}
+                hpaEnabled={false}
+                width={width}
+                height={height - 10}
+                resolution={selectedRange}
+                margin={{ top: 40, right: -40, bottom: 0, left: 50 }}
+              />
+            )}
+          </ParentSize>
+        </>
+      )}
+    </StyledMetricsSection>
+  );
+};
+
+export default Metrics;
+
+const LoadingWrapper = styled.div`
+  padding: 30px 0px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  justify-content: center;
+  color: #ffffff44;
+`;
+
+const Highlight = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-left: 8px;
+  color: ${(props: { color: string }) => props.color};
+  cursor: pointer;
+
+  > i {
+    font-size: 20px;
+    margin-right: 3px;
+  }
+`;
+
+const Label = styled.div`
+  font-weight: bold;
+`;
+
+const Relative = styled.div`
+  position: relative;
+`;
+
+const Message = styled.div`
+  display: flex;
+  height: 100%;
+  width: calc(100% - 150px);
+  align-items: center;
+  justify-content: center;
+  margin-left: 75px;
+  text-align: center;
+  color: #ffffff44;
+  font-size: 13px;
+`;
+
+const IconWrapper = styled.div`
+  display: flex;
+  position: relative;
+  align-items: center;
+  justify-content: center;
+  margin-top: 2px;
+  border-radius: 30px;
+  height: 25px;
+  width: 25px;
+  margin-left: 8px;
+  cursor: pointer;
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const SettingsIcon = styled.img`
+  opacity: 0.4;
+  width: 20px;
+  height: 20px;
+  margin-left: -1px;
+  margin-bottom: -2px;
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const MetricsHeader = styled.div`
+  width: 100%;
+  display: flex;
+  align-items: center;
+  overflow: visible;
+  justify-content: space-between;
+`;
+
+const DropdownOverlay = styled.div`
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  z-index: 10;
+  left: 0px;
+  top: 0px;
+  cursor: default;
+`;
+
+const Option = styled.div`
+  width: 100%;
+  border-top: 1px solid #00000000;
+  border-bottom: 1px solid
+    ${(props: { selected: boolean; lastItem: boolean }) =>
+      props.lastItem ? "#ffffff00" : "#ffffff15"};
+  height: 37px;
+  font-size: 13px;
+  padding-top: 9px;
+  align-items: center;
+  padding-left: 15px;
+  cursor: pointer;
+  padding-right: 10px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  background: ${(props: { selected: boolean; lastItem: boolean }) =>
+    props.selected ? "#ffffff11" : ""};
+
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const Dropdown = styled.div`
+  position: absolute;
+  left: 0;
+  top: calc(100% + 10px);
+  background: #26282f;
+  width: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) =>
+    props.dropdownWidth};
+  max-height: ${(props: { dropdownWidth: string; dropdownMaxHeight: string }) =>
+    props.dropdownMaxHeight || "300px"};
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  margin-bottom: 20px;
+  box-shadow: 0px 4px 10px 0px #00000088;
+`;
+
+const DropdownAlt = styled(Dropdown)`
+  padding: 20px 20px 7px;
+  overflow: visible;
+`;
+
+const RangeWrapper = styled.div`
+  float: right;
+  font-weight: bold;
+  width: 156px;
+  margin-top: -8px;
+`;
+
+const MetricSelector = styled.div`
+  font-size: 13px;
+  font-weight: 500;
+  position: relative;
+  color: #ffffff;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  border-radius: 5px;
+  :hover {
+    > i {
+      background: #ffffff22;
+    }
+  }
+
+  > i {
+    border-radius: 20px;
+    font-size: 20px;
+    margin-left: 10px;
+  }
+`;
+
+const MetricsLabel = styled.div`
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+  max-width: 200px;
+`;
+
+const StyledMetricsSection = styled.div`
+  width: 100%;
+  min-height: 400px;
+  height: 50vh;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  font-size: 13px;
+  border-radius: 8px;
+  border: 1px solid #ffffff33;
+  padding: 18px 22px;
+  animation: floatIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  margin-top: 20px;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
+const Header = styled.div`
+  font-weight: 500;
+  color: #aaaabb;
+  font-size: 16px;
+  margin-bottom: 15px;
+`;

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -602,6 +602,7 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
                   rightTabOptions={this.state.rightTabOptions}
                   rightTabOptions={this.state.rightTabOptions}
                   saveValuesStatus={this.state.saveValuesStatus}
                   saveValuesStatus={this.state.saveValuesStatus}
                   saveButtonText="Save Config"
                   saveButtonText="Save Config"
+                  includeHiddenFields
                 />
                 />
               )}
               )}
             </BodyWrapper>
             </BodyWrapper>

+ 18 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricNormalizer.ts

@@ -1,12 +1,13 @@
 import {
 import {
-  AvailableMetrics,
   GenericMetricResponse,
   GenericMetricResponse,
   MetricsCPUDataResponse,
   MetricsCPUDataResponse,
-  MetricsHpaReplicasDataResponse,
   MetricsMemoryDataResponse,
   MetricsMemoryDataResponse,
   MetricsNetworkDataResponse,
   MetricsNetworkDataResponse,
   MetricsNGINXErrorsDataResponse,
   MetricsNGINXErrorsDataResponse,
-  NormalizedMetricsData
+  AvailableMetrics,
+  MetricsHpaReplicasDataResponse, 
+  MetricsNGINXLatencyDataResponse,
+  NormalizedMetricsData,
 } from "./types";
 } from "./types";
 
 
 /**
 /**
@@ -39,6 +40,9 @@ export class MetricNormalizer {
     if (this.kind.includes("nginx:errors")) {
     if (this.kind.includes("nginx:errors")) {
       return this.parseNGINXErrorsMetrics(this.metric_results);
       return this.parseNGINXErrorsMetrics(this.metric_results);
     }
     }
+    if (this.kind.includes("nginx:latency") || this.kind.includes("nginx:latency-histogram")) {
+      return this.parseNGINXLatencyMetrics(this.metric_results);
+    }
     if (this.kind.includes("hpa_replicas")) {
     if (this.kind.includes("hpa_replicas")) {
       return this.parseHpaReplicaMetrics(this.metric_results);
       return this.parseHpaReplicaMetrics(this.metric_results);
     }
     }
@@ -83,6 +87,17 @@ export class MetricNormalizer {
     });
     });
   }
   }
 
 
+  private parseNGINXLatencyMetrics(
+    arr: MetricsNGINXLatencyDataResponse["results"]
+  ) {
+    return arr.map((d) => {
+      return {
+        date: d.date,
+        value: d.latency != "NaN" ? parseFloat(d.latency) : 0,
+      };
+    });
+  }
+
   private parseHpaReplicaMetrics(
   private parseHpaReplicaMetrics(
     arr: MetricsHpaReplicasDataResponse["results"]
     arr: MetricsHpaReplicasDataResponse["results"]
   ) {
   ) {

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/MetricsSection.tsx

@@ -19,14 +19,14 @@ type PropsType = {
   currentChart: ChartTypeWithExtendedConfig;
   currentChart: ChartTypeWithExtendedConfig;
 };
 };
 
 
-const resolutions: { [range: string]: string } = {
+export const resolutions: { [range: string]: string } = {
   "1H": "1s",
   "1H": "1s",
   "6H": "15s",
   "6H": "15s",
   "1D": "15s",
   "1D": "15s",
   "1M": "5h",
   "1M": "5h",
 };
 };
 
 
-const secondsBeforeNow: { [range: string]: number } = {
+export const secondsBeforeNow: { [range: string]: number } = {
   "1H": 60 * 60,
   "1H": 60 * 60,
   "6H": 60 * 60 * 6,
   "6H": 60 * 60 * 6,
   "1D": 60 * 60 * 24,
   "1D": 60 * 60 * 24,

+ 10 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/metrics/types.ts

@@ -30,6 +30,14 @@ export type MetricsNGINXErrorsDataResponse = {
   }[];
   }[];
 };
 };
 
 
+export type MetricsNGINXLatencyDataResponse = {
+  pod?: string;
+  results: {
+    date: number;
+    latency: string;
+  }[];
+};
+
 export type MetricsHpaReplicasDataResponse = {
 export type MetricsHpaReplicasDataResponse = {
   pod?: string;
   pod?: string;
   results: {
   results: {
@@ -47,6 +55,7 @@ export type GenericMetricResponse = {
     bytes: string;
     bytes: string;
     error_pct: string;
     error_pct: string;
     replicas: string;
     replicas: string;
+    latency: string;
   }[];
   }[];
 };
 };
 
 
@@ -60,6 +69,7 @@ export type AvailableMetrics =
   | "memory"
   | "memory"
   | "network"
   | "network"
   | "nginx:errors"
   | "nginx:errors"
+  | "nginx:latency"
   | "cpu_hpa_threshold"
   | "cpu_hpa_threshold"
   | "memory_hpa_threshold"
   | "memory_hpa_threshold"
   | "hpa_replicas";
   | "hpa_replicas";

+ 2 - 4
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -73,7 +73,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
     selectedRegistry: null as any,
     selectedRegistry: null as any,
   };
   };
 
 
-  createGHAction = (chartName: string, chartNamespace: string, env?: any) => {
+  createGHAction = (chartName: string, chartNamespace: string) => {
     let { currentProject, currentCluster, setCurrentError } = this.context;
     let { currentProject, currentCluster, setCurrentError } = this.context;
     let {
     let {
       actionConfig,
       actionConfig,
@@ -100,7 +100,6 @@ class LaunchFlow extends Component<PropsType, StateType> {
           folder_path: folderPath,
           folder_path: folderPath,
           image_repo_uri: imageRepoUri,
           image_repo_uri: imageRepoUri,
           git_repo_id: actionConfig.git_repo_id,
           git_repo_id: actionConfig.git_repo_id,
-          env: env,
         },
         },
         {
         {
           project_id: currentProject.id,
           project_id: currentProject.id,
@@ -314,8 +313,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
       )
       )
       .then((res: any) => {
       .then((res: any) => {
         if (sourceType === "repo") {
         if (sourceType === "repo") {
-          let env = rawValues["container.env.normal"];
-          this.createGHAction(name, selectedNamespace, env);
+          this.createGHAction(name, selectedNamespace);
         }
         }
         // this.props.setCurrentView('cluster-dashboard');
         // this.props.setCurrentView('cluster-dashboard');
         this.setState({ saveValuesStatus: "successful" }, () => {
         this.setState({ saveValuesStatus: "successful" }, () => {

+ 2 - 2
dashboard/src/shared/api.tsx

@@ -122,7 +122,6 @@ const createGHAction = baseApi<
     dockerfile_path: string;
     dockerfile_path: string;
     folder_path: string;
     folder_path: string;
     git_repo_id: number;
     git_repo_id: number;
-    env: any;
   },
   },
   {
   {
     project_id: number;
     project_id: number;
@@ -597,7 +596,8 @@ const getMetrics = baseApi<
     shouldsum: boolean;
     shouldsum: boolean;
     pods?: string[];
     pods?: string[];
     kind?: string; // the controller kind
     kind?: string; // the controller kind
-    name: string;
+    name?: string;
+    percentile?: number;
     namespace: string;
     namespace: string;
     startrange: number;
     startrange: number;
     endrange: number;
     endrange: number;

+ 21 - 9
docs/guides/running-porter-locally.md

@@ -1,19 +1,31 @@
-While it requires a few additional steps, it is possible to run Porter locally. These are the steps to start using Porter on your local machine.
+While it requires a few additional steps, it is possible to run Porter locally. Porter can either be run inside a Docker container, or the binary can be run directly.
+
+## Running the Binary
+
+To run the Porter binary, follow these steps: 
 
 
 1. [Install our CLI](https://docs.getporter.dev/docs/cli-documentation#installation)
 1. [Install our CLI](https://docs.getporter.dev/docs/cli-documentation#installation)
 
 
 2. Run `porter server start`. This will spin up a local Porter instance on port 8080.
 2. Run `porter server start`. This will spin up a local Porter instance on port 8080.
 
 
-By default, GitHub login and the deploying from GitHub repo is disabled on the local version of Porter - this is due to security reasons. However, you can add these functionalities to your local instance by creating your own GitHub OAuth application. These are the steps to enable the GitHub features on the local version of Porter:
+3. Navigate to http://localhost:8080/register, and create a new user with an email and password. 
 
 
-1. [Create a new GitHub Oauth App](https://docs.github.com/en/developers/apps/creating-an-oauth-app). This app should be created with `http://localhost:8080/api/oauth/github/callback` as the callback URL. 
+## Running with Docker
 
 
-2. Copy the Client ID and the Client secrets. Then add these lines into your `.bashrc` file:
+The easiest way to run the Docker container is to use SQLite as the persistence option. To accomplish this, you can simply run:
 
 
-```txt
-export GITHUB_CLIENT_ID=YOUR_CLIENT_ID
-export GITHUB_CLIENT_SECRET=YOUR_CLIENT_SECRET
-export GITHUB_ENABLED=true
 ```
 ```
+docker volume create porter_sqlite
+docker run \
+  --mount type=volume,source=porter_sqlite,target=/sqlite,readonly=false \
+  -e REDIS_ENABLED=false \
+  -e SQL_LITE_PATH=/sqlite/porter.db \
+  -p 8080:8080 \
+  -d porter1/porter:latest
+```
+
+Then navigate to http://localhost:8080/register, and create a new user with an email and password. 
+
+## Setting up Integrations
 
 
-3. When you run `porter server start`, Porter will automatically read these variables in and enable the GitHub features.
+While basic functionality is supported on the local binary/Docker image, more configuration is required to support various integrations. See [this document](https://docs.porter.run/docs/sso) for instructions on adding integrations like Github application access.

+ 10 - 10
internal/forms/git_action.go

@@ -7,19 +7,18 @@ import (
 // CreateGitAction represents the accepted values for creating a
 // CreateGitAction represents the accepted values for creating a
 // github action integration
 // github action integration
 type CreateGitAction struct {
 type CreateGitAction struct {
-	ReleaseID      uint              `json:"release_id" form:"required"`
-	GitRepo        string            `json:"git_repo" form:"required"`
-	GitBranch      string            `json:"git_branch"`
-	ImageRepoURI   string            `json:"image_repo_uri" form:"required"`
-	DockerfilePath string            `json:"dockerfile_path"`
-	FolderPath     string            `json:"folder_path"`
-	GitRepoID      uint              `json:"git_repo_id" form:"required"`
-	BuildEnv       map[string]string `json:"env"`
-	RegistryID     uint              `json:"registry_id"`
+	ReleaseID      uint   `json:"release_id" form:"required"`
+	GitRepo        string `json:"git_repo" form:"required"`
+	GitBranch      string `json:"git_branch"`
+	ImageRepoURI   string `json:"image_repo_uri" form:"required"`
+	DockerfilePath string `json:"dockerfile_path"`
+	FolderPath     string `json:"folder_path"`
+	GitRepoID      uint   `json:"git_repo_id" form:"required"`
+	RegistryID     uint   `json:"registry_id"`
 }
 }
 
 
 // ToGitActionConfig converts the form to a gorm git action config model
 // ToGitActionConfig converts the form to a gorm git action config model
-func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error) {
+func (ca *CreateGitAction) ToGitActionConfig(version string) (*models.GitActionConfig, error) {
 	return &models.GitActionConfig{
 	return &models.GitActionConfig{
 		ReleaseID:            ca.ReleaseID,
 		ReleaseID:            ca.ReleaseID,
 		GitRepo:              ca.GitRepo,
 		GitRepo:              ca.GitRepo,
@@ -29,6 +28,7 @@ func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error)
 		FolderPath:           ca.FolderPath,
 		FolderPath:           ca.FolderPath,
 		GithubInstallationID: ca.GitRepoID,
 		GithubInstallationID: ca.GitRepoID,
 		IsInstallation:       true,
 		IsInstallation:       true,
+		Version:              version,
 	}, nil
 	}, nil
 }
 }
 
 

+ 10 - 26
internal/integrations/ci/actions/actions.go

@@ -32,12 +32,11 @@ type GithubActions struct {
 	GithubAppSecretPath  string
 	GithubAppSecretPath  string
 	GithubInstallationID uint
 	GithubInstallationID uint
 
 
-	WebhookToken string
-	PorterToken  string
-	BuildEnv     map[string]string
-	ProjectID    uint
-	ClusterID    uint
-	ReleaseName  string
+	PorterToken string
+	BuildEnv    map[string]string
+	ProjectID   uint
+	ClusterID   uint
+	ReleaseName string
 
 
 	GitBranch      string
 	GitBranch      string
 	DockerFilePath string
 	DockerFilePath string
@@ -45,6 +44,7 @@ type GithubActions struct {
 	ImageRepoURL   string
 	ImageRepoURL   string
 
 
 	defaultBranch string
 	defaultBranch string
+	Version       string
 }
 }
 
 
 func (g *GithubActions) Setup() (string, error) {
 func (g *GithubActions) Setup() (string, error) {
@@ -67,24 +67,8 @@ func (g *GithubActions) Setup() (string, error) {
 
 
 	g.defaultBranch = repo.GetDefaultBranch()
 	g.defaultBranch = repo.GetDefaultBranch()
 
 
-	// create a new secret with a webhook token
-	err = g.createGithubSecret(client, g.getWebhookSecretName(), g.WebhookToken)
-
-	if err != nil {
-		return "", err
-	}
-
-	// create new secrets porter token, project id, and cluster id
-	err = g.createGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken)
-
-	if err != nil {
-		return "", err
-	}
-
-	// create a new secret with the build variables
-	err = g.createEnvSecret(client)
-
-	if err != nil {
+	// create porter token secret
+	if err := g.createGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken); err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
@@ -140,6 +124,7 @@ type GithubActionYAMLStep struct {
 	Timeout uint64            `yaml:"timeout-minutes,omitempty"`
 	Timeout uint64            `yaml:"timeout-minutes,omitempty"`
 	Uses    string            `yaml:"uses,omitempty"`
 	Uses    string            `yaml:"uses,omitempty"`
 	Run     string            `yaml:"run,omitempty"`
 	Run     string            `yaml:"run,omitempty"`
+	With    map[string]string `yaml:"with,omitempty"`
 	Env     map[string]string `yaml:"env,omitempty"`
 	Env     map[string]string `yaml:"env,omitempty"`
 }
 }
 
 
@@ -167,8 +152,7 @@ type GithubActionYAML struct {
 func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 	gaSteps := []GithubActionYAMLStep{
 	gaSteps := []GithubActionYAMLStep{
 		getCheckoutCodeStep(),
 		getCheckoutCodeStep(),
-		getDownloadPorterStep(),
-		getConfigurePorterStep(g.ServerURL, g.getPorterTokenSecretName(), g.ProjectID, g.ClusterID, g.ReleaseName),
+		getUpdateAppStep(g.ServerURL, g.getPorterTokenSecretName(), g.ProjectID, g.ClusterID, g.ReleaseName, g.Version),
 	}
 	}
 
 
 	branch := g.GitBranch
 	branch := g.GitBranch

+ 12 - 29
internal/integrations/ci/actions/steps.go

@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"fmt"
 )
 )
 
 
+const updateAppActionName = "porter-dev/porter-update-action"
+
 func getCheckoutCodeStep() GithubActionYAMLStep {
 func getCheckoutCodeStep() GithubActionYAMLStep {
 	return GithubActionYAMLStep{
 	return GithubActionYAMLStep{
 		Name: "Checkout code",
 		Name: "Checkout code",
@@ -11,36 +13,17 @@ func getCheckoutCodeStep() GithubActionYAMLStep {
 	}
 	}
 }
 }
 
 
-const download string = `name=$(curl -s https://api.github.com/repos/porter-dev/porter/releases/latest | grep "browser_download_url.*/porter_.*_Linux_x86_64\.zip" | cut -d ":" -f 2,3 | tr -d \")
-name=$(basename $name)
-curl -L https://github.com/porter-dev/porter/releases/latest/download/$name --output $name
-unzip -a $name
-rm $name
-chmod +x ./porter
-sudo mv ./porter /usr/local/bin/porter
-`
-
-func getDownloadPorterStep() GithubActionYAMLStep {
+func getUpdateAppStep(serverURL, porterTokenSecretName string, projectID uint, clusterID uint, appName string, actionVersion string) GithubActionYAMLStep {
 	return GithubActionYAMLStep{
 	return GithubActionYAMLStep{
-		Name: "Download Porter",
-		ID:   "download_porter",
-		Run:  download,
-	}
-}
-
-const configure string = `porter update --app %s`
-
-func getConfigurePorterStep(serverURL, porterTokenSecretName string, projectID uint, clusterID uint, appName string) GithubActionYAMLStep {
-	return GithubActionYAMLStep{
-		Name:    "Update Porter App",
-		ID:      "update_porter",
-		Run:     fmt.Sprintf(configure, appName),
-		Timeout: 20,
-		Env: map[string]string{
-			"PORTER_TOKEN":   fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
-			"PORTER_HOST":    serverURL,
-			"PORTER_PROJECT": fmt.Sprintf("%d", projectID),
-			"PORTER_CLUSTER": fmt.Sprintf("%d", clusterID),
+		Name: "Update Porter App",
+		Uses: fmt.Sprintf("%s@%s", updateAppActionName, actionVersion),
+		With: map[string]string{
+			"app":     appName,
+			"cluster": fmt.Sprintf("%d", clusterID),
+			"host":    serverURL,
+			"project": fmt.Sprintf("%d", projectID),
+			"token":   fmt.Sprintf("${{ secrets.%s }}", porterTokenSecretName),
 		},
 		},
+		Timeout: 20,
 	}
 	}
 }
 }

+ 1 - 1
internal/kubernetes/config.go

@@ -312,7 +312,7 @@ func (conf *OutOfClusterConfig) CreateRawConfigFromCluster() (*api.Config, error
 		}
 		}
 
 
 		// add this as a bearer token
 		// add this as a bearer token
-		authInfoMap[authInfoName].Token = tok
+		authInfoMap[authInfoName].Token = tok.AccessToken
 	case models.AWS:
 	case models.AWS:
 		awsAuth, err := conf.Repo.AWSIntegration.ReadAWSIntegration(
 		awsAuth, err := conf.Repo.AWSIntegration.ReadAWSIntegration(
 			cluster.AWSIntegrationID,
 			cluster.AWSIntegrationID,

+ 20 - 8
internal/kubernetes/prometheus/metrics.go

@@ -86,6 +86,7 @@ type QueryOpts struct {
 	StartRange uint     `schema:"startrange"`
 	StartRange uint     `schema:"startrange"`
 	EndRange   uint     `schema:"endrange"`
 	EndRange   uint     `schema:"endrange"`
 	Resolution string   `schema:"resolution"`
 	Resolution string   `schema:"resolution"`
+	Percentile float64  `schema:"percentile"`
 }
 }
 
 
 func QueryPrometheus(
 func QueryPrometheus(
@@ -97,7 +98,7 @@ func QueryPrometheus(
 		return nil, fmt.Errorf("prometheus service has no exposed ports to query")
 		return nil, fmt.Errorf("prometheus service has no exposed ports to query")
 	}
 	}
 
 
-	podSelectionRegex, err := getPodSelectionRegex(opts.Kind, opts.Name)
+	selectionRegex, err := getSelectionRegex(opts.Kind, opts.Name)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -108,7 +109,7 @@ func QueryPrometheus(
 	if len(opts.PodList) > 0 {
 	if len(opts.PodList) > 0 {
 		podSelector = fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, strings.Join(opts.PodList, "|"))
 		podSelector = fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, strings.Join(opts.PodList, "|"))
 	} else {
 	} else {
-		podSelector = fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, podSelectionRegex)
+		podSelector = fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, selectionRegex)
 	}
 	}
 
 
 	query := ""
 	query := ""
@@ -118,12 +119,18 @@ func QueryPrometheus(
 	} else if opts.Metric == "memory" {
 	} else if opts.Metric == "memory" {
 		query = fmt.Sprintf("container_memory_usage_bytes{%s}", podSelector)
 		query = fmt.Sprintf("container_memory_usage_bytes{%s}", podSelector)
 	} else if opts.Metric == "network" {
 	} else if opts.Metric == "network" {
-		netPodSelector := fmt.Sprintf(`namespace="%s",pod=~"%s",container="POD"`, opts.Namespace, podSelectionRegex)
+		netPodSelector := fmt.Sprintf(`namespace="%s",pod=~"%s",container="POD"`, opts.Namespace, selectionRegex)
 		query = fmt.Sprintf("rate(container_network_receive_bytes_total{%s}[5m])", netPodSelector)
 		query = fmt.Sprintf("rate(container_network_receive_bytes_total{%s}[5m])", netPodSelector)
 	} else if opts.Metric == "nginx:errors" {
 	} else if opts.Metric == "nginx:errors" {
-		num := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%s",ingress=~"%s"}[5m]) OR on() vector(0))`, opts.Namespace, podSelectionRegex)
-		denom := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{namespace="%s",ingress=~"%s"}[5m]) > 0)`, opts.Namespace, podSelectionRegex)
+		num := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%s",ingress=~"%s"}[5m]) OR on() vector(0))`, opts.Namespace, selectionRegex)
+		denom := fmt.Sprintf(`sum(rate(nginx_ingress_controller_requests{namespace="%s",ingress=~"%s"}[5m]) > 0)`, opts.Namespace, selectionRegex)
 		query = fmt.Sprintf(`%s / %s * 100 OR on() vector(0)`, num, denom)
 		query = fmt.Sprintf(`%s / %s * 100 OR on() vector(0)`, num, denom)
+	} else if opts.Metric == "nginx:latency" {
+		num := fmt.Sprintf(`sum(rate(nginx_ingress_controller_request_duration_seconds_sum{namespace=~"%s",ingress=~"%s"}[5m]) OR on() vector(0))`, opts.Namespace, selectionRegex)
+		denom := fmt.Sprintf(`sum(rate(nginx_ingress_controller_request_duration_seconds_count{namespace=~"%s",ingress=~"%s"}[5m]))`, opts.Namespace, selectionRegex)
+		query = fmt.Sprintf(`%s / %s OR on() vector(0)`, num, denom)
+	} else if opts.Metric == "nginx:latency-histogram" {
+		query = fmt.Sprintf(`histogram_quantile(%f, sum(rate(nginx_ingress_controller_request_duration_seconds_bucket{status!="404",status!="500",namespace=~"%s",ingress=~"%s"}[5m])) by (le, ingress))`, opts.Percentile, opts.Namespace, selectionRegex)
 	} else if opts.Metric == "cpu_hpa_threshold" {
 	} else if opts.Metric == "cpu_hpa_threshold" {
 		// get the name of the kube hpa metric
 		// get the name of the kube hpa metric
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "spec_target_metric")
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "spec_target_metric")
@@ -135,7 +142,7 @@ func QueryPrometheus(
 			appLabel = ksmSvc.ObjectMeta.Labels["app.kubernetes.io/instance"]
 			appLabel = ksmSvc.ObjectMeta.Labels["app.kubernetes.io/instance"]
 		}
 		}
 
 
-		query = createHPAAbsoluteCPUThresholdQuery(cpuMetricName, metricName, podSelectionRegex, opts.Name, opts.Namespace, appLabel, hpaMetricName)
+		query = createHPAAbsoluteCPUThresholdQuery(cpuMetricName, metricName, selectionRegex, opts.Name, opts.Namespace, appLabel, hpaMetricName)
 	} else if opts.Metric == "memory_hpa_threshold" {
 	} else if opts.Metric == "memory_hpa_threshold" {
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "spec_target_metric")
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "spec_target_metric")
 		memMetricName := getKubeMemoryMetricName(clientset, service, opts)
 		memMetricName := getKubeMemoryMetricName(clientset, service, opts)
@@ -146,7 +153,7 @@ func QueryPrometheus(
 			appLabel = ksmSvc.ObjectMeta.Labels["app.kubernetes.io/instance"]
 			appLabel = ksmSvc.ObjectMeta.Labels["app.kubernetes.io/instance"]
 		}
 		}
 
 
-		query = createHPAAbsoluteMemoryThresholdQuery(memMetricName, metricName, podSelectionRegex, opts.Name, opts.Namespace, appLabel, hpaMetricName)
+		query = createHPAAbsoluteMemoryThresholdQuery(memMetricName, metricName, selectionRegex, opts.Name, opts.Namespace, appLabel, hpaMetricName)
 	} else if opts.Metric == "hpa_replicas" {
 	} else if opts.Metric == "hpa_replicas" {
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "status_current_replicas")
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "status_current_replicas")
 		ksmSvc, found, _ := getKubeStateMetricsService(clientset)
 		ksmSvc, found, _ := getKubeStateMetricsService(clientset)
@@ -208,6 +215,7 @@ type promParsedSingletonQueryResult struct {
 	Memory   interface{} `json:"memory,omitempty"`
 	Memory   interface{} `json:"memory,omitempty"`
 	Bytes    interface{} `json:"bytes,omitempty"`
 	Bytes    interface{} `json:"bytes,omitempty"`
 	ErrorPct interface{} `json:"error_pct,omitempty"`
 	ErrorPct interface{} `json:"error_pct,omitempty"`
+	Latency  interface{} `json:"latency,omitempty"`
 }
 }
 
 
 type promParsedSingletonQuery struct {
 type promParsedSingletonQuery struct {
@@ -248,6 +256,8 @@ func parseQuery(rawQuery []byte, metric string) ([]byte, error) {
 				singletonResult.Memory = values[1]
 				singletonResult.Memory = values[1]
 			} else if metric == "hpa_replicas" {
 			} else if metric == "hpa_replicas" {
 				singletonResult.Replicas = values[1]
 				singletonResult.Replicas = values[1]
+			} else if metric == "nginx:latency" || metric == "nginx:latency-histogram" {
+				singletonResult.Latency = values[1]
 			}
 			}
 
 
 			singletonResults = append(singletonResults, *singletonResult)
 			singletonResults = append(singletonResults, *singletonResult)
@@ -261,7 +271,7 @@ func parseQuery(rawQuery []byte, metric string) ([]byte, error) {
 	return json.Marshal(res)
 	return json.Marshal(res)
 }
 }
 
 
-func getPodSelectionRegex(kind, name string) (string, error) {
+func getSelectionRegex(kind, name string) (string, error) {
 	var suffix string
 	var suffix string
 
 
 	switch strings.ToLower(kind) {
 	switch strings.ToLower(kind) {
@@ -273,6 +283,8 @@ func getPodSelectionRegex(kind, name string) (string, error) {
 		suffix = "[a-z0-9]+"
 		suffix = "[a-z0-9]+"
 	case "cronjob":
 	case "cronjob":
 		suffix = "[a-z0-9]+-[a-z0-9]+"
 		suffix = "[a-z0-9]+-[a-z0-9]+"
+	case "ingress":
+		return name, nil
 	default:
 	default:
 		return "", fmt.Errorf("not a supported controller to query for metrics")
 		return "", fmt.Errorf("not a supported controller to query for metrics")
 	}
 	}

+ 2 - 0
internal/models/gitrepo.go

@@ -75,6 +75,8 @@ type GitActionConfig struct {
 
 
 	// Determines on how authentication is performed on this action
 	// Determines on how authentication is performed on this action
 	IsInstallation bool `json:"is_installation"`
 	IsInstallation bool `json:"is_installation"`
+
+	Version string `json:"version" gorm:"default:v0.0.1"`
 }
 }
 
 
 // GitActionConfigExternal is an external GitActionConfig to be shared over REST
 // GitActionConfigExternal is an external GitActionConfig to be shared over REST

+ 9 - 5
internal/models/integrations/gcp.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
 
 
+	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
 	"golang.org/x/oauth2/google"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 )
 )
@@ -83,13 +84,16 @@ func (g *GCPIntegration) GetBearerToken(
 	getTokenCache GetTokenCacheFunc,
 	getTokenCache GetTokenCacheFunc,
 	setTokenCache SetTokenCacheFunc,
 	setTokenCache SetTokenCacheFunc,
 	scopes ...string,
 	scopes ...string,
-) (string, error) {
+) (*oauth2.Token, error) {
 	cache, err := getTokenCache()
 	cache, err := getTokenCache()
 
 
 	// check the token cache for a non-expired token
 	// check the token cache for a non-expired token
 	if cache != nil {
 	if cache != nil {
 		if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
 		if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
-			return string(tok), nil
+			return &oauth2.Token{
+				AccessToken: string(cache.Token),
+				Expiry:      cache.Expiry,
+			}, nil
 		}
 		}
 	}
 	}
 
 
@@ -100,19 +104,19 @@ func (g *GCPIntegration) GetBearerToken(
 	)
 	)
 
 
 	if err != nil {
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 	}
 
 
 	tok, err := creds.TokenSource.Token()
 	tok, err := creds.TokenSource.Token()
 
 
 	if err != nil {
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 	}
 
 
 	// update the token cache
 	// update the token cache
 	setTokenCache(tok.AccessToken, tok.Expiry)
 	setTokenCache(tok.AccessToken, tok.Expiry)
 
 
-	return tok.AccessToken, nil
+	return tok, nil
 }
 }
 
 
 // credentialsFile is the unmarshalled representation of a GCP credentials file.
 // credentialsFile is the unmarshalled representation of a GCP credentials file.

+ 16 - 7
internal/registry/registry.go

@@ -92,6 +92,8 @@ type gcrRepositoryResp struct {
 }
 }
 
 
 func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, error) {
 func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, error) {
+	getTokenCache := r.getTokenCacheFunc(repo)
+
 	gcp, err := repo.GCPIntegration.ReadGCPIntegration(
 	gcp, err := repo.GCPIntegration.ReadGCPIntegration(
 		r.GCPIntegrationID,
 		r.GCPIntegrationID,
 	)
 	)
@@ -102,7 +104,7 @@ func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, er
 
 
 	// get oauth2 access token
 	// get oauth2 access token
 	_, err = gcp.GetBearerToken(
 	_, err = gcp.GetBearerToken(
-		r.getTokenCache,
+		getTokenCache,
 		r.setTokenCacheFunc(repo),
 		r.setTokenCacheFunc(repo),
 		"https://www.googleapis.com/auth/devstorage.read_write",
 		"https://www.googleapis.com/auth/devstorage.read_write",
 	)
 	)
@@ -112,7 +114,7 @@ func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, er
 	}
 	}
 
 
 	// it's now written to the token cache, so return
 	// it's now written to the token cache, so return
-	cache, err := r.getTokenCache()
+	cache, err := getTokenCache()
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -352,11 +354,18 @@ func (r *Registry) listPrivateRegistryRepositories(
 	return res, nil
 	return res, nil
 }
 }
 
 
-func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
-	return &ints.TokenCache{
-		Token:  r.TokenCache.Token,
-		Expiry: r.TokenCache.Expiry,
-	}, nil
+func (r *Registry) getTokenCacheFunc(
+	repo repository.Repository,
+) ints.GetTokenCacheFunc {
+	return func() (tok *ints.TokenCache, err error) {
+		reg, err := repo.Registry.ReadRegistry(r.ID)
+
+		if err != nil {
+			return nil, err
+		}
+
+		return &reg.TokenCache.TokenCache, nil
+	}
 }
 }
 
 
 func (r *Registry) setTokenCacheFunc(
 func (r *Registry) setTokenCacheFunc(

+ 5 - 1
server/api/api.go

@@ -316,7 +316,11 @@ func (app *App) getTokenFromRequest(r *http.Request) *token.Token {
 
 
 	reqToken = strings.TrimSpace(splitToken[1])
 	reqToken = strings.TrimSpace(splitToken[1])
 
 
-	tok, _ := token.GetTokenFromEncoded(reqToken, app.tokenConf)
+	tok, err := token.GetTokenFromEncoded(reqToken, app.tokenConf)
+
+	if err != nil {
+		return nil
+	}
 
 
 	return tok
 	return tok
 }
 }

+ 0 - 2
server/api/deploy_handler.go

@@ -167,7 +167,6 @@ func (app *App) HandleDeployTemplate(w http.ResponseWriter, r *http.Request) {
 			ImageRepoURI:   form.GithubActionConfig.ImageRepoURI,
 			ImageRepoURI:   form.GithubActionConfig.ImageRepoURI,
 			DockerfilePath: form.GithubActionConfig.DockerfilePath,
 			DockerfilePath: form.GithubActionConfig.DockerfilePath,
 			GitRepoID:      form.GithubActionConfig.GitRepoID,
 			GitRepoID:      form.GithubActionConfig.GitRepoID,
-			BuildEnv:       form.GithubActionConfig.BuildEnv,
 			RegistryID:     form.GithubActionConfig.RegistryID,
 			RegistryID:     form.GithubActionConfig.RegistryID,
 		}
 		}
 
 
@@ -381,7 +380,6 @@ func (app *App) HandleUninstallTemplate(w http.ResponseWriter, r *http.Request)
 					GitRepoOwner:           repoSplit[0],
 					GitRepoOwner:           repoSplit[0],
 					Repo:                   *app.Repo,
 					Repo:                   *app.Repo,
 					GithubConf:             app.GithubProjectConf,
 					GithubConf:             app.GithubProjectConf,
-					WebhookToken:           release.WebhookToken,
 					ProjectID:              uint(projID),
 					ProjectID:              uint(projID),
 					ReleaseName:            name,
 					ReleaseName:            name,
 					GitBranch:              gitAction.GitBranch,
 					GitBranch:              gitAction.GitBranch,

+ 6 - 3
server/api/git_action_handler.go

@@ -16,6 +16,10 @@ import (
 	"github.com/porter-dev/porter/internal/registry"
 	"github.com/porter-dev/porter/internal/registry"
 )
 )
 
 
+const (
+	updateAppActionVersion = "v0.1.0"
+)
+
 // HandleCreateGitAction creates a new Github action in a repository for a given
 // HandleCreateGitAction creates a new Github action in a repository for a given
 // release
 // release
 func (app *App) HandleCreateGitAction(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleCreateGitAction(w http.ResponseWriter, r *http.Request) {
@@ -108,7 +112,7 @@ func (app *App) createGitActionFromForm(
 	}
 	}
 
 
 	// convert the form to a git action config
 	// convert the form to a git action config
-	gitAction, err := form.ToGitActionConfig()
+	gitAction, err := form.ToGitActionConfig(updateAppActionVersion)
 
 
 	if err != nil {
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
@@ -165,7 +169,6 @@ func (app *App) createGitActionFromForm(
 		GitRepoOwner:           repoSplit[0],
 		GitRepoOwner:           repoSplit[0],
 		Repo:                   *app.Repo,
 		Repo:                   *app.Repo,
 		GithubConf:             app.GithubProjectConf,
 		GithubConf:             app.GithubProjectConf,
-		WebhookToken:           release.WebhookToken,
 		ProjectID:              uint(projID),
 		ProjectID:              uint(projID),
 		ReleaseName:            name,
 		ReleaseName:            name,
 		GitBranch:              gitAction.GitBranch,
 		GitBranch:              gitAction.GitBranch,
@@ -173,8 +176,8 @@ func (app *App) createGitActionFromForm(
 		FolderPath:             gitAction.FolderPath,
 		FolderPath:             gitAction.FolderPath,
 		ImageRepoURL:           gitAction.ImageRepoURI,
 		ImageRepoURL:           gitAction.ImageRepoURI,
 		PorterToken:            encoded,
 		PorterToken:            encoded,
-		BuildEnv:               form.BuildEnv,
 		ClusterID:              release.ClusterID,
 		ClusterID:              release.ClusterID,
+		Version:                gitAction.Version,
 	}
 	}
 
 
 	_, err = gaRunner.Setup()
 	_, err = gaRunner.Setup()

+ 8 - 3
server/api/git_repo_handler.go

@@ -4,8 +4,6 @@ import (
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
-	"github.com/porter-dev/porter/internal/models"
-	"golang.org/x/oauth2"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"regexp"
 	"regexp"
@@ -13,6 +11,9 @@ import (
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 
 
+	"github.com/porter-dev/porter/internal/models"
+	"golang.org/x/oauth2"
+
 	"github.com/bradleyfalzon/ghinstallation"
 	"github.com/bradleyfalzon/ghinstallation"
 	"github.com/go-chi/chi"
 	"github.com/go-chi/chi"
 	"github.com/google/go-github/github"
 	"github.com/google/go-github/github"
@@ -20,10 +21,13 @@ import (
 
 
 // HandleListProjectGitRepos returns a list of git repos for a project
 // HandleListProjectGitRepos returns a list of git repos for a project
 func (app *App) HandleListProjectGitRepos(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleListProjectGitRepos(w http.ResponseWriter, r *http.Request) {
-
 	tok, err := app.getGithubAppOauthTokenFromRequest(r)
 	tok, err := app.getGithubAppOauthTokenFromRequest(r)
 
 
 	if err != nil {
 	if err != nil {
+		app.Logger.Warn().Err(err).
+			Str("info", "github app oauth token error").
+			Msg("")
+
 		json.NewEncoder(w).Encode(make([]*models.GitRepoExternal, 0))
 		json.NewEncoder(w).Encode(make([]*models.GitRepoExternal, 0))
 		return
 		return
 	}
 	}
@@ -35,6 +39,7 @@ func (app *App) HandleListProjectGitRepos(w http.ResponseWriter, r *http.Request
 	AuthUser, _, err := client.Users.Get(context.Background(), "")
 	AuthUser, _, err := client.Users.Get(context.Background(), "")
 
 
 	if err != nil {
 	if err != nil {
+
 		app.handleErrorInternal(err, w)
 		app.handleErrorInternal(err, w)
 		return
 		return
 	}
 	}

+ 8 - 7
server/api/integration_handler.go

@@ -7,12 +7,6 @@ import (
 	"encoding/hex"
 	"encoding/hex"
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
-	"github.com/go-chi/chi"
-	"github.com/google/go-github/github"
-	"github.com/porter-dev/porter/internal/forms"
-	"github.com/porter-dev/porter/internal/oauth"
-	"golang.org/x/oauth2"
-	"gorm.io/gorm"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
@@ -20,6 +14,13 @@ import (
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
+	"github.com/go-chi/chi"
+	"github.com/google/go-github/github"
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/oauth"
+	"golang.org/x/oauth2"
+	"gorm.io/gorm"
+
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 )
 )
@@ -583,7 +584,7 @@ func (app *App) getGithubAppOauthTokenFromRequest(r *http.Request) (*oauth2.Toke
 	oauthInt, err := app.Repo.GithubAppOAuthIntegration.ReadGithubAppOauthIntegration(user.GithubAppIntegrationID)
 	oauthInt, err := app.Repo.GithubAppOAuthIntegration.ReadGithubAppOauthIntegration(user.GithubAppIntegrationID)
 
 
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("Could not get GH app integration for user %d: %s", user.ID, err.Error())
 	}
 	}
 
 
 	_, _, err = oauth.GetAccessToken(oauthInt.SharedOAuthModel,
 	_, _, err = oauth.GetAccessToken(oauthInt.SharedOAuthModel,

+ 28 - 14
server/api/release_handler.go

@@ -40,6 +40,10 @@ const (
 	ErrReleaseDeploy
 	ErrReleaseDeploy
 )
 )
 
 
+var (
+	createEnvSecretConstraint, _ = semver.NewConstraint(" < 0.1.0")
+)
+
 // HandleListReleases retrieves a list of releases for a cluster
 // HandleListReleases retrieves a list of releases for a cluster
 // with various filter options
 // with various filter options
 func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
@@ -1130,7 +1134,6 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 					GitRepoOwner:           repoSplit[0],
 					GitRepoOwner:           repoSplit[0],
 					Repo:                   *app.Repo,
 					Repo:                   *app.Repo,
 					GithubConf:             app.GithubProjectConf,
 					GithubConf:             app.GithubProjectConf,
-					WebhookToken:           release.WebhookToken,
 					ProjectID:              uint(projID),
 					ProjectID:              uint(projID),
 					ReleaseName:            name,
 					ReleaseName:            name,
 					GitBranch:              gitAction.GitBranch,
 					GitBranch:              gitAction.GitBranch,
@@ -1139,15 +1142,21 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 					ImageRepoURL:           gitAction.ImageRepoURI,
 					ImageRepoURL:           gitAction.ImageRepoURI,
 					BuildEnv:               cEnv.Container.Env.Normal,
 					BuildEnv:               cEnv.Container.Env.Normal,
 					ClusterID:              release.ClusterID,
 					ClusterID:              release.ClusterID,
+					Version:                gitAction.Version,
 				}
 				}
 
 
-				err = gaRunner.CreateEnvSecret()
-
+				actionVersion, err := semver.NewVersion(gaRunner.Version)
 				if err != nil {
 				if err != nil {
-					app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
-						Code:   ErrReleaseReadData,
-						Errors: []string{"could not update github secret"},
-					}, w)
+					app.handleErrorInternal(err, w)
+				}
+
+				if createEnvSecretConstraint.Check(actionVersion) {
+					if err := gaRunner.CreateEnvSecret(); err != nil {
+						app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+							Code:   ErrReleaseReadData,
+							Errors: []string{"could not update github secret"},
+						}, w)
+					}
 				}
 				}
 			}
 			}
 		}
 		}
@@ -1563,7 +1572,6 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 					GitRepoOwner:           repoSplit[0],
 					GitRepoOwner:           repoSplit[0],
 					Repo:                   *app.Repo,
 					Repo:                   *app.Repo,
 					GithubConf:             app.GithubProjectConf,
 					GithubConf:             app.GithubProjectConf,
-					WebhookToken:           release.WebhookToken,
 					ProjectID:              uint(projID),
 					ProjectID:              uint(projID),
 					ReleaseName:            name,
 					ReleaseName:            name,
 					GitBranch:              gitAction.GitBranch,
 					GitBranch:              gitAction.GitBranch,
@@ -1572,15 +1580,21 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 					ImageRepoURL:           gitAction.ImageRepoURI,
 					ImageRepoURL:           gitAction.ImageRepoURI,
 					BuildEnv:               cEnv.Container.Env.Normal,
 					BuildEnv:               cEnv.Container.Env.Normal,
 					ClusterID:              release.ClusterID,
 					ClusterID:              release.ClusterID,
+					Version:                gitAction.Version,
 				}
 				}
 
 
-				err = gaRunner.CreateEnvSecret()
-
+				actionVersion, err := semver.NewVersion(gaRunner.Version)
 				if err != nil {
 				if err != nil {
-					app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
-						Code:   ErrReleaseReadData,
-						Errors: []string{"could not update github secret"},
-					}, w)
+					app.handleErrorInternal(err, w)
+				}
+
+				if createEnvSecretConstraint.Check(actionVersion) {
+					if err := gaRunner.CreateEnvSecret(); err != nil {
+						app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
+							Code:   ErrReleaseReadData,
+							Errors: []string{"could not update github secret"},
+						}, w)
+					}
 				}
 				}
 			}
 			}
 		}
 		}

+ 1 - 1
server/api/user_handler.go

@@ -848,7 +848,7 @@ func (app *App) getUserIDFromRequest(r *http.Request) (uint, error) {
 	session, err := app.Store.Get(r, app.ServerConf.CookieName)
 	session, err := app.Store.Get(r, app.ServerConf.CookieName)
 
 
 	if err != nil {
 	if err != nil {
-		return 0, err
+		return 0, fmt.Errorf("could not get session: %s", err.Error())
 	}
 	}
 
 
 	sessID, ok := session.Values["user_id"]
 	sessID, ok := session.Values["user_id"]