Quellcode durchsuchen

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

Ivan Galakhov vor 4 Jahren
Ursprung
Commit
8f429e00d0
32 geänderte Dateien mit 779 neuen und 154 gelöschten Zeilen
  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
 // a Github action
 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

+ 9 - 0
cli/cmd/create.go

@@ -74,6 +74,7 @@ var name string
 var values string
 var source string
 var image string
+var registryURL string
 
 func init() {
 	rootCmd.AddCommand(createCmd)
@@ -137,6 +138,13 @@ func init() {
 		"",
 		"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": ""}
@@ -183,6 +191,7 @@ func createFull(resp *api.AuthCheckResponse, client *api.Client, args []string)
 			},
 			Kind:        args[0],
 			ReleaseName: name,
+			RegistryURL: registryURL,
 		},
 	}
 

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

@@ -25,6 +25,7 @@ type CreateOpts struct {
 
 	Kind        string
 	ReleaseName string
+	RegistryURL string
 }
 
 // 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
 
 	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
 			imageURI = fmt.Sprintf("%s/%s-%s", reg.URL, name, namespace)
 			break

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

@@ -200,12 +200,14 @@ func (a *Agent) PushImage(image string) error {
 		opts,
 	)
 
+	if out != nil {
+		defer out.Close()
+	}
+
 	if err != nil {
 		return err
 	}
 
-	defer out.Close()
-
 	termFd, isTerm := term.GetFdInfo(os.Stderr)
 
 	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
-var Version string = "v0.5.0"
+var Version string = "v0.8.0"
 
 var versionCmd = &cobra.Command{
 	Use:     "version",

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

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

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

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

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

@@ -1,7 +1,11 @@
 import React from "react";
 import InputRow from "../../form-components/InputRow";
 import useFormField from "../hooks/useFormField";
-import { GetFinalVariablesFunction, InputField, StringInputFieldState } from "../types";
+import {
+  GetFinalVariablesFunction,
+  InputField,
+  StringInputFieldState,
+} from "../types";
 
 const clipOffUnit = (unit: string, x: string) => {
   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 ClusterSettings from "./ClusterSettings";
 import useAuth from "shared/auth/useAuth";
+import Metrics from "./Metrics";
 
-type TabEnum = "nodes" | "settings" | "namespaces";
+type TabEnum = "nodes" | "settings" | "namespaces" | "metrics";
 
 const tabOptions: {
   label: string;
   value: TabEnum;
 }[] = [
   { label: "Nodes", value: "nodes" },
+  { label: "Metrics", value: "metrics" },
   { label: "Namespaces", value: "namespaces" },
   { label: "Settings", value: "settings" },
 ];
@@ -32,6 +34,8 @@ export const Dashboard: React.FunctionComponent = () => {
     switch (currentTab) {
       case "settings":
         return <ClusterSettings />;
+      case "metrics":
+        return <Metrics />;
       case "namespaces":
         return <NamespaceList />;
       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}
                   saveValuesStatus={this.state.saveValuesStatus}
                   saveButtonText="Save Config"
+                  includeHiddenFields
                 />
               )}
             </BodyWrapper>

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

@@ -1,12 +1,13 @@
 import {
-  AvailableMetrics,
   GenericMetricResponse,
   MetricsCPUDataResponse,
-  MetricsHpaReplicasDataResponse,
   MetricsMemoryDataResponse,
   MetricsNetworkDataResponse,
   MetricsNGINXErrorsDataResponse,
-  NormalizedMetricsData
+  AvailableMetrics,
+  MetricsHpaReplicasDataResponse, 
+  MetricsNGINXLatencyDataResponse,
+  NormalizedMetricsData,
 } from "./types";
 
 /**
@@ -39,6 +40,9 @@ export class MetricNormalizer {
     if (this.kind.includes("nginx:errors")) {
       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")) {
       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(
     arr: MetricsHpaReplicasDataResponse["results"]
   ) {

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

@@ -19,14 +19,14 @@ type PropsType = {
   currentChart: ChartTypeWithExtendedConfig;
 };
 
-const resolutions: { [range: string]: string } = {
+export const resolutions: { [range: string]: string } = {
   "1H": "1s",
   "6H": "15s",
   "1D": "15s",
   "1M": "5h",
 };
 
-const secondsBeforeNow: { [range: string]: number } = {
+export const secondsBeforeNow: { [range: string]: number } = {
   "1H": 60 * 60,
   "6H": 60 * 60 * 6,
   "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 = {
   pod?: string;
   results: {
@@ -47,6 +55,7 @@ export type GenericMetricResponse = {
     bytes: string;
     error_pct: string;
     replicas: string;
+    latency: string;
   }[];
 };
 
@@ -60,6 +69,7 @@ export type AvailableMetrics =
   | "memory"
   | "network"
   | "nginx:errors"
+  | "nginx:latency"
   | "cpu_hpa_threshold"
   | "memory_hpa_threshold"
   | "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,
   };
 
-  createGHAction = (chartName: string, chartNamespace: string, env?: any) => {
+  createGHAction = (chartName: string, chartNamespace: string) => {
     let { currentProject, currentCluster, setCurrentError } = this.context;
     let {
       actionConfig,
@@ -100,7 +100,6 @@ class LaunchFlow extends Component<PropsType, StateType> {
           folder_path: folderPath,
           image_repo_uri: imageRepoUri,
           git_repo_id: actionConfig.git_repo_id,
-          env: env,
         },
         {
           project_id: currentProject.id,
@@ -314,8 +313,7 @@ class LaunchFlow extends Component<PropsType, StateType> {
       )
       .then((res: any) => {
         if (sourceType === "repo") {
-          let env = rawValues["container.env.normal"];
-          this.createGHAction(name, selectedNamespace, env);
+          this.createGHAction(name, selectedNamespace);
         }
         // this.props.setCurrentView('cluster-dashboard');
         this.setState({ saveValuesStatus: "successful" }, () => {

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

@@ -122,7 +122,6 @@ const createGHAction = baseApi<
     dockerfile_path: string;
     folder_path: string;
     git_repo_id: number;
-    env: any;
   },
   {
     project_id: number;
@@ -597,7 +596,8 @@ const getMetrics = baseApi<
     shouldsum: boolean;
     pods?: string[];
     kind?: string; // the controller kind
-    name: string;
+    name?: string;
+    percentile?: number;
     namespace: string;
     startrange: 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)
 
 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
 // github action integration
 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
-func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error) {
+func (ca *CreateGitAction) ToGitActionConfig(version string) (*models.GitActionConfig, error) {
 	return &models.GitActionConfig{
 		ReleaseID:            ca.ReleaseID,
 		GitRepo:              ca.GitRepo,
@@ -29,6 +28,7 @@ func (ca *CreateGitAction) ToGitActionConfig() (*models.GitActionConfig, error)
 		FolderPath:           ca.FolderPath,
 		GithubInstallationID: ca.GitRepoID,
 		IsInstallation:       true,
+		Version:              version,
 	}, nil
 }
 

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

@@ -32,12 +32,11 @@ type GithubActions struct {
 	GithubAppSecretPath  string
 	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
 	DockerFilePath string
@@ -45,6 +44,7 @@ type GithubActions struct {
 	ImageRepoURL   string
 
 	defaultBranch string
+	Version       string
 }
 
 func (g *GithubActions) Setup() (string, error) {
@@ -67,24 +67,8 @@ func (g *GithubActions) Setup() (string, error) {
 
 	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
 	}
 
@@ -140,6 +124,7 @@ type GithubActionYAMLStep struct {
 	Timeout uint64            `yaml:"timeout-minutes,omitempty"`
 	Uses    string            `yaml:"uses,omitempty"`
 	Run     string            `yaml:"run,omitempty"`
+	With    map[string]string `yaml:"with,omitempty"`
 	Env     map[string]string `yaml:"env,omitempty"`
 }
 
@@ -167,8 +152,7 @@ type GithubActionYAML struct {
 func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 	gaSteps := []GithubActionYAMLStep{
 		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

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

@@ -4,6 +4,8 @@ import (
 	"fmt"
 )
 
+const updateAppActionName = "porter-dev/porter-update-action"
+
 func getCheckoutCodeStep() GithubActionYAMLStep {
 	return GithubActionYAMLStep{
 		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{
-		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
-		authInfoMap[authInfoName].Token = tok
+		authInfoMap[authInfoName].Token = tok.AccessToken
 	case models.AWS:
 		awsAuth, err := conf.Repo.AWSIntegration.ReadAWSIntegration(
 			cluster.AWSIntegrationID,

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

@@ -86,6 +86,7 @@ type QueryOpts struct {
 	StartRange uint     `schema:"startrange"`
 	EndRange   uint     `schema:"endrange"`
 	Resolution string   `schema:"resolution"`
+	Percentile float64  `schema:"percentile"`
 }
 
 func QueryPrometheus(
@@ -97,7 +98,7 @@ func QueryPrometheus(
 		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 {
 		return nil, err
@@ -108,7 +109,7 @@ func QueryPrometheus(
 	if len(opts.PodList) > 0 {
 		podSelector = fmt.Sprintf(`namespace="%s",pod=~"%s",container!="POD",container!=""`, opts.Namespace, strings.Join(opts.PodList, "|"))
 	} 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 := ""
@@ -118,12 +119,18 @@ func QueryPrometheus(
 	} else if opts.Metric == "memory" {
 		query = fmt.Sprintf("container_memory_usage_bytes{%s}", podSelector)
 	} 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)
 	} 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)
+	} 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" {
 		// get the name of the kube hpa metric
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "spec_target_metric")
@@ -135,7 +142,7 @@ func QueryPrometheus(
 			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" {
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "spec_target_metric")
 		memMetricName := getKubeMemoryMetricName(clientset, service, opts)
@@ -146,7 +153,7 @@ func QueryPrometheus(
 			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" {
 		metricName, hpaMetricName := getKubeHPAMetricName(clientset, service, opts, "status_current_replicas")
 		ksmSvc, found, _ := getKubeStateMetricsService(clientset)
@@ -208,6 +215,7 @@ type promParsedSingletonQueryResult struct {
 	Memory   interface{} `json:"memory,omitempty"`
 	Bytes    interface{} `json:"bytes,omitempty"`
 	ErrorPct interface{} `json:"error_pct,omitempty"`
+	Latency  interface{} `json:"latency,omitempty"`
 }
 
 type promParsedSingletonQuery struct {
@@ -248,6 +256,8 @@ func parseQuery(rawQuery []byte, metric string) ([]byte, error) {
 				singletonResult.Memory = values[1]
 			} else if metric == "hpa_replicas" {
 				singletonResult.Replicas = values[1]
+			} else if metric == "nginx:latency" || metric == "nginx:latency-histogram" {
+				singletonResult.Latency = values[1]
 			}
 
 			singletonResults = append(singletonResults, *singletonResult)
@@ -261,7 +271,7 @@ func parseQuery(rawQuery []byte, metric string) ([]byte, error) {
 	return json.Marshal(res)
 }
 
-func getPodSelectionRegex(kind, name string) (string, error) {
+func getSelectionRegex(kind, name string) (string, error) {
 	var suffix string
 
 	switch strings.ToLower(kind) {
@@ -273,6 +283,8 @@ func getPodSelectionRegex(kind, name string) (string, error) {
 		suffix = "[a-z0-9]+"
 	case "cronjob":
 		suffix = "[a-z0-9]+-[a-z0-9]+"
+	case "ingress":
+		return name, nil
 	default:
 		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
 	IsInstallation bool `json:"is_installation"`
+
+	Version string `json:"version" gorm:"default:v0.0.1"`
 }
 
 // GitActionConfigExternal is an external GitActionConfig to be shared over REST

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

@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 
+	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
 	"gorm.io/gorm"
 )
@@ -83,13 +84,16 @@ func (g *GCPIntegration) GetBearerToken(
 	getTokenCache GetTokenCacheFunc,
 	setTokenCache SetTokenCacheFunc,
 	scopes ...string,
-) (string, error) {
+) (*oauth2.Token, error) {
 	cache, err := getTokenCache()
 
 	// check the token cache for a non-expired token
 	if cache != nil {
 		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 {
-		return "", err
+		return nil, err
 	}
 
 	tok, err := creds.TokenSource.Token()
 
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
 	// update the token cache
 	setTokenCache(tok.AccessToken, tok.Expiry)
 
-	return tok.AccessToken, nil
+	return tok, nil
 }
 
 // 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) {
+	getTokenCache := r.getTokenCacheFunc(repo)
+
 	gcp, err := repo.GCPIntegration.ReadGCPIntegration(
 		r.GCPIntegrationID,
 	)
@@ -102,7 +104,7 @@ func (r *Registry) GetGCRToken(repo repository.Repository) (*ints.TokenCache, er
 
 	// get oauth2 access token
 	_, err = gcp.GetBearerToken(
-		r.getTokenCache,
+		getTokenCache,
 		r.setTokenCacheFunc(repo),
 		"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
-	cache, err := r.getTokenCache()
+	cache, err := getTokenCache()
 
 	if err != nil {
 		return nil, err
@@ -352,11 +354,18 @@ func (r *Registry) listPrivateRegistryRepositories(
 	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(

+ 5 - 1
server/api/api.go

@@ -316,7 +316,11 @@ func (app *App) getTokenFromRequest(r *http.Request) *token.Token {
 
 	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
 }

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

+ 6 - 3
server/api/git_action_handler.go

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

+ 8 - 3
server/api/git_repo_handler.go

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

+ 8 - 7
server/api/integration_handler.go

@@ -7,12 +7,6 @@ import (
 	"encoding/hex"
 	"encoding/json"
 	"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"
 	"net/http"
 	"net/url"
@@ -20,6 +14,13 @@ import (
 	"strconv"
 	"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"
 	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)
 
 	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,

+ 28 - 14
server/api/release_handler.go

@@ -40,6 +40,10 @@ const (
 	ErrReleaseDeploy
 )
 
+var (
+	createEnvSecretConstraint, _ = semver.NewConstraint(" < 0.1.0")
+)
+
 // HandleListReleases retrieves a list of releases for a cluster
 // with various filter options
 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],
 					Repo:                   *app.Repo,
 					GithubConf:             app.GithubProjectConf,
-					WebhookToken:           release.WebhookToken,
 					ProjectID:              uint(projID),
 					ReleaseName:            name,
 					GitBranch:              gitAction.GitBranch,
@@ -1139,15 +1142,21 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 					ImageRepoURL:           gitAction.ImageRepoURI,
 					BuildEnv:               cEnv.Container.Env.Normal,
 					ClusterID:              release.ClusterID,
+					Version:                gitAction.Version,
 				}
 
-				err = gaRunner.CreateEnvSecret()
-
+				actionVersion, err := semver.NewVersion(gaRunner.Version)
 				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],
 					Repo:                   *app.Repo,
 					GithubConf:             app.GithubProjectConf,
-					WebhookToken:           release.WebhookToken,
 					ProjectID:              uint(projID),
 					ReleaseName:            name,
 					GitBranch:              gitAction.GitBranch,
@@ -1572,15 +1580,21 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 					ImageRepoURL:           gitAction.ImageRepoURI,
 					BuildEnv:               cEnv.Container.Env.Normal,
 					ClusterID:              release.ClusterID,
+					Version:                gitAction.Version,
 				}
 
-				err = gaRunner.CreateEnvSecret()
-
+				actionVersion, err := semver.NewVersion(gaRunner.Version)
 				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)
 
 	if err != nil {
-		return 0, err
+		return 0, fmt.Errorf("could not get session: %s", err.Error())
 	}
 
 	sessID, ok := session.Values["user_id"]