Ian Edwards 2 سال پیش
والد
کامیت
24aafedf40

+ 28 - 7
dashboard/package-lock.json

@@ -12,7 +12,7 @@
         "@loadable/component": "^5.15.2",
         "@material-ui/core": "^4.11.3",
         "@material-ui/lab": "^4.0.0-alpha.61",
-        "@porter-dev/api-contracts": "^0.0.75",
+        "@porter-dev/api-contracts": "^0.0.81",
         "@sentry/react": "^6.13.2",
         "@sentry/tracing": "^6.13.2",
         "@tanstack/react-query": "^4.13.0",
@@ -59,6 +59,7 @@
         "react-diff-viewer": "^3.1.1",
         "react-dom": "^18.0.0",
         "react-error-boundary": "^3.1.3",
+        "react-hook-form": "^7.45.4",
         "react-hot-toast": "^2.4.0",
         "react-infinite-scroll-component": "^6.1.0",
         "react-modal": "^3.11.2",
@@ -2434,9 +2435,9 @@
       }
     },
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.0.75",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.75.tgz",
-      "integrity": "sha512-IHyLUCRPWyEwH0fljC6iu6134yGGki4qXHHa7j8wYOSEWMOed/r86OIpXY0tdNpYhDKAcPaoxlDwPbbeV6Wj0Q==",
+      "version": "0.0.81",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.81.tgz",
+      "integrity": "sha512-OTkble7ir8G0RrnLyFc7TwCmTi2Em6GaxkQX2e+eBCPpyPOW8jm5TSI6spOcQ4ILD+1hyKqRoMvmOMDYCYPKwg==",
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
       }
@@ -10946,6 +10947,21 @@
       "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz",
       "integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg=="
     },
+    "node_modules/react-hook-form": {
+      "version": "7.45.4",
+      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.4.tgz",
+      "integrity": "sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ==",
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/react-hook-form"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17 || ^18"
+      }
+    },
     "node_modules/react-hot-toast": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.0.tgz",
@@ -16672,9 +16688,9 @@
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "@porter-dev/api-contracts": {
-      "version": "0.0.75",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.75.tgz",
-      "integrity": "sha512-IHyLUCRPWyEwH0fljC6iu6134yGGki4qXHHa7j8wYOSEWMOed/r86OIpXY0tdNpYhDKAcPaoxlDwPbbeV6Wj0Q==",
+      "version": "0.0.81",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.81.tgz",
+      "integrity": "sha512-OTkble7ir8G0RrnLyFc7TwCmTi2Em6GaxkQX2e+eBCPpyPOW8jm5TSI6spOcQ4ILD+1hyKqRoMvmOMDYCYPKwg==",
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"
       }
@@ -23638,6 +23654,11 @@
       "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz",
       "integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg=="
     },
+    "react-hook-form": {
+      "version": "7.45.4",
+      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.4.tgz",
+      "integrity": "sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ=="
+    },
     "react-hot-toast": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.0.tgz",

+ 2 - 1
dashboard/package.json

@@ -7,7 +7,7 @@
     "@loadable/component": "^5.15.2",
     "@material-ui/core": "^4.11.3",
     "@material-ui/lab": "^4.0.0-alpha.61",
-    "@porter-dev/api-contracts": "^0.0.75",
+    "@porter-dev/api-contracts": "^0.0.81",
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",
     "@tanstack/react-query": "^4.13.0",
@@ -54,6 +54,7 @@
     "react-diff-viewer": "^3.1.1",
     "react-dom": "^18.0.0",
     "react-error-boundary": "^3.1.3",
+    "react-hook-form": "^7.45.4",
     "react-hot-toast": "^2.4.0",
     "react-infinite-scroll-component": "^6.1.0",
     "react-modal": "^3.11.2",

+ 118 - 0
dashboard/src/components/porter/ControlledInput.tsx

@@ -0,0 +1,118 @@
+import React from "react";
+import styled from "styled-components";
+
+export const ControlledInput = React.forwardRef<
+  HTMLInputElement,
+  {
+    id: string;
+    name: string;
+    label?: string;
+    type: React.HTMLInputTypeAttribute;
+    autoComplete: string;
+    placeholder?: string;
+    defaultValue?: string;
+    disabled?: boolean;
+    onChange: React.ChangeEventHandler<HTMLInputElement>;
+    onBlur: React.FocusEventHandler<HTMLInputElement>;
+    width?: string;
+    height?: string;
+    error?: string;
+  }
+>(
+  (
+    {
+      id,
+      name,
+      label,
+      type,
+      autoComplete,
+      placeholder,
+      defaultValue,
+      disabled,
+      onChange,
+      onBlur,
+      width,
+      height,
+      error,
+    },
+    ref
+  ) => (
+    <Block width={width}>
+      {label && <Label>{label}</Label>}
+      <StyledInput
+        id={id}
+        name={name}
+        type={type}
+        autoComplete={autoComplete}
+        placeholder={placeholder}
+        defaultValue={defaultValue}
+        onChange={onChange}
+        onBlur={onBlur}
+        ref={ref}
+        disabled={disabled}
+        width={width}
+        height={height}
+        hasError={(error && true) || error === ""}
+      />
+      {error && (
+        <Error>
+          <i className="material-icons">error</i>
+          {error}
+        </Error>
+      )}
+    </Block>
+  )
+);
+
+ControlledInput.displayName = "ControlledInput";
+
+const Block = styled.div<{
+  width?: string;
+}>`
+  display: block;
+  position: relative;
+  width: ${(props) => props.width || "200px"};
+`;
+
+const Label = styled.div`
+  font-size: 13px;
+  color: #aaaabb;
+  margin-bottom: 10px;
+`;
+
+const Error = styled.div`
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  color: #ff3b62;
+  margin-top: 10px;
+  > i {
+    font-size: 18px;
+    margin-right: 5px;
+  }
+`;
+
+const StyledInput = styled.input<{
+  hasError: boolean;
+  width?: string;
+  height?: string;
+  disabled?: boolean;
+}>`
+  height: ${(props) => props.height || "35px"};
+  padding: 5px 10px;
+  width: ${(props) => props.width || "200px"};
+  color: ${(props) => (props.disabled ? "#aaaabb" : "#ffffff")};
+  font-size: 13px;
+  outline: none;
+  border-radius: 5px;
+  background: #26292e;
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "")};
+  border: 1px solid ${(props) => (props.hasError ? "#ff3b62" : "#494b4f")};
+  ${(props) =>
+    !props.disabled &&
+    `
+      :hover {
+        border: 1px solid ${props.hasError ? "#ff3b62" : "#7a7b80"};
+      }
+    `}
+`;

+ 19 - 14
dashboard/src/main/home/Home.tsx

@@ -40,6 +40,7 @@ import Button from "components/porter/Button";
 import NewAppFlow from "./app-dashboard/new-app-flow/NewAppFlow";
 import ExpandedApp from "./app-dashboard/expanded-app/ExpandedApp";
 import ExpandedJob from "./app-dashboard/expanded-app/expanded-job/ExpandedJob";
+import CreateApplication from "./app-dashboard/new-app-flow/create-application/CreateApplication";
 
 // Guarded components
 const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
@@ -192,7 +193,7 @@ const Home: React.FC<Props> = (props) => {
       } else {
         setHasFinishedOnboarding(true);
       }
-    } catch (error) { }
+    } catch (error) {}
   };
 
   useEffect(() => {
@@ -334,7 +335,11 @@ const Home: React.FC<Props> = (props) => {
 
     localStorage.removeItem(currentProject.id + "-cluster");
     try {
-      await api.updateOnboardingStep("<token>", { step: "project-delete" }, { project_id: currentProject.id });
+      await api.updateOnboardingStep(
+        "<token>",
+        { step: "project-delete" },
+        { project_id: currentProject.id }
+      );
       await api.deleteProject("<token>", {}, { id: currentProject.id });
       projectOverlayCall();
     } catch (error) {
@@ -410,7 +415,7 @@ const Home: React.FC<Props> = (props) => {
 
           <Switch>
             <Route path="/apps/new/app">
-              <NewAppFlow />
+              <CreateApplication />
             </Route>
             <Route path="/apps/:appName/:tab">
               <ExpandedApp />
@@ -443,17 +448,17 @@ const Home: React.FC<Props> = (props) => {
               overrideInfraTabEnabled({
                 projectID: currentProject?.id,
               })) && (
-                <Route
-                  path="/infrastructure"
-                  render={() => {
-                    return (
-                      <DashboardWrapper>
-                        <InfrastructureRouter />
-                      </DashboardWrapper>
-                    );
-                  }}
-                />
-              )}
+              <Route
+                path="/infrastructure"
+                render={() => {
+                  return (
+                    <DashboardWrapper>
+                      <InfrastructureRouter />
+                    </DashboardWrapper>
+                  );
+                }}
+              />
+            )}
             <Route
               path="/dashboard"
               render={() => {

+ 205 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/CreateApplication.tsx

@@ -0,0 +1,205 @@
+import React, { useContext, useEffect } from "react";
+import { RouteComponentProps, withRouter } from "react-router";
+import web from "assets/web.png";
+
+import styled from "styled-components";
+import { PorterApp } from "@porter-dev/api-contracts";
+import { useForm, Controller, FormProvider } from "react-hook-form";
+import Back from "components/porter/Back";
+import DashboardHeader from "../../../cluster-dashboard/DashboardHeader";
+import VerticalSteps from "components/porter/VerticalSteps";
+import Text from "components/porter/Text";
+import Spacer from "components/porter/Spacer";
+import { ControlledInput } from "components/porter/ControlledInput";
+import Link from "components/porter/Link";
+import SourceSelector from "../SourceSelector";
+import SourceSetup from "./SourceSetup";
+import RepoBuildSettings from "./RepoBuildSettings";
+import { Context } from "shared/Context";
+import { Buildpack } from "../../types/buildpack";
+
+type CreateApplicationProps = {} & RouteComponentProps;
+
+export type SourceOptions =
+  | {
+      type: "github";
+      git_repo_name: string;
+      git_repo_id: number;
+      git_branch: string;
+      porter_yaml_path: string;
+    }
+  | {
+      type: "docker-registry";
+      image_repo_uri: string;
+    };
+
+export type Build = {
+  build_context: string;
+  builder: string;
+  buildpacks: Buildpack[];
+  dockerfile: string;
+};
+
+export type PorterAppFormData = PorterApp & {
+  source: SourceOptions;
+  build: Build;
+};
+
+const CreateApplication: React.FC<CreateApplicationProps> = ({}) => {
+  const { currentProject } = useContext(Context);
+  const [step, setStep] = React.useState(0);
+
+  const methods = useForm<PorterAppFormData>({
+    reValidateMode: "onSubmit",
+    defaultValues: {
+      name: "",
+      source: {
+        git_repo_name: "",
+        git_repo_id: 0,
+        git_branch: "",
+        porter_yaml_path: "./porter.yaml",
+      },
+      build: {
+        build_context: "./",
+        builder: "",
+        buildpacks: [],
+        dockerfile: "",
+      },
+    },
+  });
+  const { register, control, watch } = methods;
+
+  const name = watch("name");
+  const source = watch("source");
+  const build = watch("build");
+
+  useEffect(() => {
+    if (name) {
+      setStep((prev) => Math.max(prev, 1));
+    }
+  }, [name]);
+
+  if (!currentProject) {
+    return null;
+  }
+
+  return (
+    <CenterWrapper>
+      <Div>
+        <StyledConfigureTemplate>
+          <Back to="/apps" />
+          <DashboardHeader
+            prefix={<Icon src={web} />}
+            title="Deploy a new application"
+            capitalize={false}
+            disableLineBreak
+          />
+          <DarkMatter />
+          <FormProvider {...methods}>
+            <VerticalSteps
+              currentStep={step}
+              steps={[
+                <>
+                  <Text size={16}>Application name</Text>
+                  <Spacer y={0.5} />
+                  <Text color="helper">
+                    Lowercase letters, numbers, and "-" only.
+                  </Text>
+                  <Spacer y={0.5} />
+                  <ControlledInput
+                    id={"name"}
+                    placeholder="ex: academic-sophon"
+                    autoComplete="off"
+                    type="text"
+                    {...register("name")}
+                  />
+                </>,
+                <>
+                  <Text size={16}>Deployment method</Text>
+                  <Spacer y={0.5} />
+                  <Text color="helper">
+                    Deploy from a Git repository or a Docker registry.
+                    <Spacer inline width="5px" />
+                    <Link
+                      hasunderline
+                      to="https://docs.porter.run/standard/deploying-applications/overview"
+                      target="_blank"
+                    >
+                      Learn more
+                    </Link>
+                  </Text>
+                  <Spacer y={0.5} />
+                  <Controller
+                    name="source.type"
+                    control={control}
+                    render={({ field: { value, onChange } }) => (
+                      <SourceSelector
+                        selectedSourceType={value}
+                        setSourceType={(sourceType) => {
+                          onChange(sourceType);
+                        }}
+                      />
+                    )}
+                  />
+                  {source?.type ? (
+                    source.type === "github" ? (
+                      <RepoBuildSettings
+                        build={build}
+                        source={source}
+                        projectId={currentProject.id}
+                      />
+                    ) : (
+                      <div></div>
+                    )
+                  ) : null}
+                </>,
+              ]}
+            />
+          </FormProvider>
+          <Spacer y={3} />
+        </StyledConfigureTemplate>
+      </Div>
+    </CenterWrapper>
+  );
+};
+
+export default withRouter(CreateApplication);
+
+const Div = styled.div`
+  width: 100%;
+  max-width: 900px;
+`;
+
+const CenterWrapper = styled.div`
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+`;
+
+const DarkMatter = styled.div`
+  width: 100%;
+  margin-top: -5px;
+`;
+
+const StyledConfigureTemplate = styled.div`
+  height: 100%;
+`;
+
+const Icon = styled.img`
+  margin-right: 15px;
+  height: 28px;
+  animation: floatIn 0.5s;
+  animation-fill-mode: forwards;
+
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(20px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;

+ 398 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/RepoBuildSettings.tsx

@@ -0,0 +1,398 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { Build, PorterAppFormData, SourceOptions } from "./CreateApplication";
+import { useQuery } from "@tanstack/react-query";
+import { BuildMethod, PorterApp } from "../../types/porterApp";
+import api from "shared/api";
+import { Controller, useFormContext } from "react-hook-form";
+import Text from "components/porter/Text";
+import Spacer from "components/porter/Spacer";
+import styled from "styled-components";
+import RepositorySelector from "../../build-settings/RepositorySelector";
+import Input from "components/porter/Input";
+import BranchSelector from "../../build-settings/BranchSelector";
+import { ControlledInput } from "components/porter/ControlledInput";
+import Select from "components/porter/Select";
+import AnimateHeight from "react-animate-height";
+import { z } from "zod";
+import BuildpackSettings from "./buildpacks/BuildpackSettings";
+
+type Props = {
+  projectId: number;
+  source: SourceOptions & { type: "github" };
+  build: Build;
+};
+
+const branchContentsSchema = z
+  .object({
+    path: z.string(),
+    type: z.enum(["file", "dir"]),
+  })
+  .array();
+
+type BranchContents = z.infer<typeof branchContentsSchema>;
+
+const RepoBuildSettings: React.FC<Props> = ({ projectId, source, build }) => {
+  const {
+    watch,
+    control,
+    register,
+    setValue,
+  } = useFormContext<PorterAppFormData>();
+  const [buildView, setBuildView] = useState<BuildMethod>("buildpacks");
+  const [showSettings, setShowSettings] = useState<boolean>(false);
+
+  const repoIsSet = useMemo(() => source.git_repo_name !== "", [
+    source.git_repo_name,
+  ]);
+  const branchIsSet = useMemo(() => source.git_branch !== "", [
+    source.git_branch,
+  ]);
+
+  const { owner, name } = useMemo(() => {
+    const split = source.git_repo_name.split("/");
+    if (split.length !== 2) {
+      return { owner: "", name: "" };
+    }
+    return { owner: split[0], name: split[1] };
+  }, [source.git_repo_name]);
+
+  const { data: yamlContents, status: yamlStatus } = useQuery(
+    [
+      "getPorterYamlContents",
+      projectId,
+      source.git_branch,
+      source.git_repo_name,
+    ],
+    async () => {
+      const res = await api.getPorterYamlContents(
+        "<token>",
+        { path: source.porter_yaml_path },
+        {
+          project_id: projectId,
+          git_repo_id: source.git_repo_id,
+          kind: "github",
+          owner,
+          name,
+          branch: source.git_branch,
+        }
+      );
+
+      return atob(res.data);
+    },
+    {
+      enabled: repoIsSet && branchIsSet,
+    }
+  );
+  const {
+    data: branchContents,
+    status: branchContentsStatus,
+  } = useQuery<BranchContents>(
+    ["getBranchContents", projectId, source.git_branch, source.git_repo_name],
+    async () => {
+      const res = await api.getBranchContents(
+        "<token>",
+        { dir: build.build_context || "./" },
+        {
+          project_id: projectId,
+          git_repo_id: source.git_repo_id,
+          kind: "github",
+          owner: source.git_repo_name.split("/")[0],
+          name: source.git_repo_name.split("/")[1],
+          branch: source.git_branch,
+        }
+      );
+
+      return branchContentsSchema.parse(res.data);
+    },
+    {
+      enabled: repoIsSet && branchIsSet,
+    }
+  );
+
+  useEffect(() => {
+    if (!branchContents) {
+      return;
+    }
+
+    const hasDockerfile = branchContents.some((item) =>
+      item.path.includes("Dockerfile")
+    );
+    setBuildView(hasDockerfile ? "docker" : "buildpacks");
+  }, [branchContents]);
+
+  return (
+    <div>
+      <Text size={16}>Build settings</Text>
+      <Spacer y={0.5} />
+      <Text color="helper">Specify your GitHub repository.</Text>
+      <Spacer y={0.5} />
+
+      {!source.git_repo_name && (
+        <Controller
+          name="source.git_repo_name"
+          control={control}
+          render={({ field: { onChange } }) => (
+            <>
+              <ExpandedWrapper>
+                <RepositorySelector
+                  readOnly={false}
+                  updatePorterApp={(pa) => {
+                    onChange(pa.repo_name);
+                    setValue(
+                      "source.git_repo_id",
+                      pa.git_repo_id ? pa.git_repo_id : 0
+                    );
+                  }}
+                  git_repo_name={source.git_repo_name}
+                />
+              </ExpandedWrapper>
+              <DarkMatter antiHeight="-4px" />
+              <Spacer y={0.3} />
+            </>
+          )}
+        />
+      )}
+
+      {!!source.git_repo_name && (
+        <>
+          <Input
+            disabled={true}
+            label="GitHub repository:"
+            width="100%"
+            value={source.git_repo_name}
+            setValue={() => {}}
+            placeholder=""
+          />
+          <BackButton
+            width="135px"
+            onClick={() => {
+              setValue("source", {
+                type: "github",
+                git_repo_name: "",
+                git_branch: "",
+                git_repo_id: 0,
+                porter_yaml_path: "./porter.yaml",
+              });
+
+              setValue("build.build_context", "./");
+            }}
+          >
+            <i className="material-icons">keyboard_backspace</i>
+            Select repo
+          </BackButton>
+          <Spacer y={0.5} />
+          <Spacer y={0.5} />
+          <Text color="helper">Specify your GitHub branch.</Text>
+          <Spacer y={0.5} />
+          {!source.git_branch && (
+            <Controller
+              name="source.git_branch"
+              control={control}
+              render={({ field: { onChange } }) => (
+                <ExpandedWrapper>
+                  <BranchSelector
+                    setBranch={(branch: string) => onChange(branch)}
+                    repo_name={source.git_repo_name}
+                    git_repo_id={source.git_repo_id}
+                  />
+                </ExpandedWrapper>
+              )}
+            />
+          )}
+          {!!source.git_branch && (
+            <>
+              <Input
+                disabled={true}
+                label="GitHub branch:"
+                type="text"
+                width="100%"
+                value={source.git_branch}
+                setValue={() => {}}
+                placeholder=""
+              />
+              <BackButton
+                width="145px"
+                onClick={() => {
+                  setValue("source", {
+                    ...source,
+                    git_branch: "",
+                    porter_yaml_path: "./porter.yaml",
+                  });
+
+                  setValue("build.build_context", "./");
+                }}
+              >
+                <i className="material-icons">keyboard_backspace</i>
+                Select branch
+              </BackButton>
+              <Spacer y={1} />
+              <Text color="helper">Specify your application root path.</Text>
+              <Spacer y={0.5} />
+              <ControlledInput
+                id={"build_context"}
+                placeholder="ex: ./"
+                autoComplete="off"
+                width="100%"
+                type="text"
+                {...register("build.build_context")}
+              />
+              <Spacer y={1} />
+              <StyledAdvancedBuildSettings
+                showSettings={showSettings}
+                isCurrent={true}
+                onClick={() => {
+                  setShowSettings(!showSettings);
+                }}
+              >
+                {buildView == "docker" ? (
+                  <AdvancedBuildTitle>
+                    <i className="material-icons dropdown">arrow_drop_down</i>
+                    Configure Dockerfile settings
+                  </AdvancedBuildTitle>
+                ) : (
+                  <AdvancedBuildTitle>
+                    <i className="material-icons dropdown">arrow_drop_down</i>
+                    Configure buildpack settings
+                  </AdvancedBuildTitle>
+                )}
+              </StyledAdvancedBuildSettings>
+
+              <AnimateHeight height={showSettings ? "auto" : 0} duration={1000}>
+                <StyledSourceBox>
+                  <Select
+                    value={buildView}
+                    width="300px"
+                    options={[
+                      { value: "docker", label: "Docker" },
+                      { value: "buildpacks", label: "Buildpacks" },
+                    ]}
+                    setValue={(option: string) =>
+                      setBuildView(option as BuildMethod)
+                    }
+                    label="Build method"
+                  />
+                  {buildView === "docker" ? (
+                    <>
+                      <Spacer y={0.5} />
+                      <Text color="helper">
+                        Dockerfile path (absolute path)
+                      </Text>
+                      <Spacer y={0.5} />
+                      <ControlledInput
+                        width="300px"
+                        id={"dockerfile"}
+                        placeholder="ex: ./Dockerfile"
+                        autoComplete="off"
+                        type="text"
+                        {...register("build.dockerfile")}
+                      />
+                      <Spacer y={0.5} />
+                    </>
+                  ) : (
+                    <BuildpackSettings
+                      projectId={projectId}
+                      build={build}
+                      source={source}
+                      
+                    />
+                  )}
+                </StyledSourceBox>
+              </AnimateHeight>
+            </>
+          )}
+        </>
+      )}
+    </div>
+  );
+};
+
+export default RepoBuildSettings;
+
+const DarkMatter = styled.div<{ antiHeight?: string }>`
+  width: 100%;
+  margin-top: ${(props) => props.antiHeight || "-15px"};
+`;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  max-height: 275px;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 22px;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 13px;
+  margin-bottom: -7px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;
+
+const StyledAdvancedBuildSettings = styled.div`
+  color: ${({ showSettings }) => (showSettings ? "white" : "#aaaabb")};
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+    color: white;
+  }
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-radius: 5px;
+  height: 40px;
+  font-size: 13px;
+  width: 100%;
+  padding-left: 10px;
+  cursor: pointer;
+  border-bottom-left-radius: ${({ showSettings }) => showSettings && "0px"};
+  border-bottom-right-radius: ${({ showSettings }) => showSettings && "0px"};
+
+  .dropdown {
+    margin-right: 8px;
+    font-size: 20px;
+    cursor: pointer;
+    border-radius: 20px;
+    transform: ${(props: { showSettings: boolean; isCurrent: boolean }) =>
+      props.showSettings ? "" : "rotate(-90deg)"};
+  }
+`;
+
+const AdvancedBuildTitle = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const StyledSourceBox = styled.div`
+  width: 100%;
+  color: #ffffff;
+  padding: 25px 35px 25px;
+  position: relative;
+  font-size: 13px;
+  border-radius: 5px;
+  background: ${(props) => props.theme.fg};
+  border: 1px solid #494b4f;
+  border-top: 0px;
+  border-top-left-radius: 0px;
+  border-top-right-radius: 0px;
+`;

+ 220 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/SourceSetup.tsx

@@ -0,0 +1,220 @@
+import AnimateHeight from "react-animate-height";
+import React, { useContext } from "react";
+import Spacer from "components/porter/Spacer";
+import styled from "styled-components";
+import { RouteComponentProps, withRouter } from "react-router";
+
+import { Controller, useFormContext } from "react-hook-form";
+import { PorterAppFormData, SourceOptions } from "./CreateApplication";
+import Text from "components/porter/Text";
+import RepositorySelector from "../../build-settings/RepositorySelector";
+import BranchSelector from "../../build-settings/BranchSelector";
+import Input from "components/porter/Input";
+import { ControlledInput } from "components/porter/ControlledInput";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { useQuery } from "@tanstack/react-query";
+
+type Props = RouteComponentProps & {
+  source: SourceOptions;
+};
+
+const SourceSettings: React.FC<Props> = ({ source, location, history }) => {
+  const { currentProject } = useContext(Context);
+  const { watch, control, register } = useFormContext<PorterAppFormData>();
+  const { data, status } = useQuery(
+    source.type === "github"
+      ? [
+          "getPorterYamlContents",
+          currentProject?.id,
+          source.git_branch,
+          source.git_repo_name,
+        ]
+      : [],
+    async () => {
+      if (!currentProject) {
+        return;
+      }
+      if (source.type !== "github") {
+        return;
+      }
+      const res = await api.getPorterYamlContents(
+        "<token>",
+        {
+          path: source.porter_yaml_path,
+        },
+        {
+          project_id: currentProject.id,
+          git_repo_id: source.git_repo_id,
+          kind: "github",
+          owner: source.git_repo_name.split("/")[0],
+          name: source.git_repo_name.split("/")[1],
+          branch: source.git_branch,
+        }
+      );
+
+      return res;
+    },
+    {
+      enabled:
+        source.type === "github" &&
+        Boolean(source.git_repo_name) &&
+        Boolean(source.git_branch),
+    }
+  );
+
+  const renderSettings = ({
+    source,
+    onChange,
+  }: {
+    source: SourceOptions;
+    onChange: (fn: (prev: SourceOptions) => SourceOptions) => void;
+  }) => {
+    if (source.type === "github") {
+      return (
+        <>
+          <Text size={16}>Build settings</Text>
+          <Spacer y={0.5} />
+          <Text color="helper">Specify your GitHub repository.</Text>
+          <Spacer y={0.5} />
+
+          {source.git_repo_name !== "" ? (
+            <>
+              <Text size={16}>Build settings</Text>
+              <Spacer y={0.5} />
+              <Text color="helper">Specify your GitHub branch.</Text>
+              <Spacer y={0.5} />
+              {source.git_branch === "" ? (
+                <BranchSelector
+                  setBranch={(branch: string) => {
+                    onChange((prev: SourceOptions) => ({
+                      ...prev,
+                      git_branch: branch,
+                    }));
+                  }}
+                  repo_name={source.git_repo_name}
+                  git_repo_id={source.git_repo_id}
+                />
+              ) : (
+                <>
+                  <Input
+                    disabled={true}
+                    label="GitHub branch:"
+                    type="text"
+                    width="100%"
+                    value={source.git_branch}
+                    setValue={() => {}}
+                    placeholder=""
+                  />
+                  <BackButton width="">
+                    <i className="material-icons">keyboard_backspace</i>
+                    Select branch
+                  </BackButton>
+                  <Spacer y={1} />
+                  <Text color="helper">
+                    Specify your application root path.
+                  </Text>
+                  <Spacer y={0.5} />
+                  <ControlledInput
+                    id={"build_context"}
+                    placeholder="ex: ./"
+                    autoComplete="off"
+                    type="text"
+                    {...register("build.build_context")}
+                  />
+                  <Spacer y={0.5} />
+                  <Spacer y={1} />
+                </>
+              )}
+            </>
+          ) : null}
+        </>
+      );
+    }
+
+    return <div></div>;
+  };
+
+  return (
+    <SourceSettingsContainer>
+      <AnimateHeight height={source ? "auto" : 0}>
+        <Spacer y={1} />
+        <Controller
+          name="source"
+          control={control}
+          render={({ field: { value: source, onChange } }) =>
+            renderSettings({
+              source,
+              onChange,
+            })
+          }
+        />
+      </AnimateHeight>
+    </SourceSettingsContainer>
+  );
+};
+
+export default withRouter(SourceSettings);
+
+const SourceSettingsContainer = styled.div``;
+
+const DarkMatter = styled.div<{ antiHeight?: string }>`
+  width: 100%;
+  margin-top: ${(props) => props.antiHeight || "-15px"};
+`;
+
+const Subtitle = styled.div`
+  padding: 11px 0px 16px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  line-height: 1.6em;
+`;
+
+const StyledSourceBox = styled.div`
+  width: 100%;
+  color: #ffffff;
+  padding: 14px 35px 20px;
+  position: relative;
+  font-size: 13px;
+  margin-top: 6px;
+  margin-bottom: 25px;
+  border-radius: 5px;
+  background: ${(props) => props.theme.fg};
+  border: 1px solid #494b4f;
+`;
+
+const ExpandedWrapper = styled.div`
+  margin-top: 10px;
+  width: 100%;
+  border-radius: 3px;
+  max-height: 275px;
+`;
+
+const BackButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 22px;
+  cursor: pointer;
+  font-size: 13px;
+  height: 35px;
+  padding: 5px 13px;
+  margin-bottom: -7px;
+  padding-right: 15px;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  width: ${(props: { width: string }) => props.width};
+  color: white;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: white;
+    font-size: 16px;
+    margin-right: 6px;
+  }
+`;

+ 150 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/buildpacks/AddCustomBuildpack.tsx

@@ -0,0 +1,150 @@
+import InputRow from "components/form-components/InputRow";
+import { Buildpack } from "main/home/app-dashboard/types/buildpack";
+import React, { useState } from "react";
+import styled, { keyframes } from "styled-components";
+
+function isValidURL(url: string): boolean {
+  const pattern = /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,})(:\d{2,5})?([\/\w.-]*)*\/?$/i;
+  return pattern.test(url);
+}
+
+const AddCustomBuildpackComponent: React.FC<{
+  onAdd: (buildpack: Buildpack) => void;
+}> = ({ onAdd }) => {
+  const [buildpackUrl, setBuildpackUrl] = useState("");
+  const [error, setError] = useState(false);
+
+  const handleAddCustomBuildpack = () => {
+    if (buildpackUrl === "" || !isValidURL(buildpackUrl)) {
+      setError(true);
+      return;
+    }
+    setBuildpackUrl("");
+    onAdd({
+      buildpack: buildpackUrl,
+      name: buildpackUrl,
+      config: {},
+    });
+  };
+
+  return (
+    <StyledCard marginBottom="0px">
+      <ContentContainer>
+        <EventInformation>
+          <BuildpackInputContainer>
+            GitHub or ZIP URL
+            <BuildpackUrlInput
+              placeholder="https://github.com/custom/buildpack"
+              type="input"
+              value={buildpackUrl}
+              isRequired
+              setValue={(newUrl) => {
+                setError(false);
+                setBuildpackUrl(newUrl as string);
+              }}
+            />
+            <ErrorText hasError={error}>Please enter a valid url</ErrorText>
+          </BuildpackInputContainer>
+        </EventInformation>
+      </ContentContainer>
+      <ActionContainer>
+        <ActionButton onClick={() => handleAddCustomBuildpack()}>
+          <span className="material-icons-outlined">add</span>
+        </ActionButton>
+      </ActionContainer>
+    </StyledCard>
+  );
+};
+
+export default AddCustomBuildpackComponent;
+
+const fadeIn = keyframes`
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+`;
+
+const StyledCard = styled.div<{ marginBottom?: string }>`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border: 1px solid #494b4f;
+  background: ${({ theme }) => theme.fg};
+  margin-bottom: ${(props) => props.marginBottom || "30px"};
+  border-radius: 8px;
+  padding: 14px;
+  overflow: hidden;
+  height: 60px;
+  font-size: 13px;
+  animation: ${fadeIn} 0.5s;
+`;
+
+const ContentContainer = styled.div`
+  display: flex;
+  height: 100%;
+  width: 100%;
+  align-items: center;
+`;
+
+const EventInformation = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+  height: 100%;
+`;
+
+const BuildpackInputContainer = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+  padding-left: 15px;
+`;
+
+const BuildpackUrlInput = styled(InputRow)`
+  width: auto;
+  min-width: 300px;
+  max-width: 600px;
+  margin: unset;
+  margin-left: 10px;
+  display: inline-block;
+`;
+
+const ErrorText = styled.span`
+  color: red;
+  margin-left: 10px;
+  display: ${(props: { hasError: boolean }) =>
+    props.hasError ? "inline-block" : "none"};
+`;
+
+const ActionContainer = styled.div`
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+  height: 100%;
+`;
+
+const ActionButton = styled.button`
+  position: relative;
+  border: none;
+  background: none;
+  color: white;
+  padding: 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  cursor: pointer;
+  color: #aaaabb;
+
+  :hover {
+    background: #ffffff11;
+    border: 1px solid #ffffff44;
+  }
+
+  > span {
+    font-size: 20px;
+  }
+`;

+ 159 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/buildpacks/BuildpackCard.tsx

@@ -0,0 +1,159 @@
+import React from "react";
+import { DeviconsNameList } from "assets/devicons-name-list";
+import styled, { keyframes } from "styled-components";
+import { Draggable } from "react-beautiful-dnd";
+import { Buildpack } from "main/home/app-dashboard/types/buildpack";
+
+interface Props {
+  buildpack: Buildpack;
+  action: "add" | "remove";
+  onClickFn: (buildpack: string) => void;
+  index: number;
+  draggable: boolean;
+}
+
+const BuildpackCard: React.FC<Props> = ({
+  buildpack,
+  action,
+  onClickFn,
+  index,
+  draggable,
+}) => {
+  const [languageName] = buildpack.name?.split("/").reverse();
+
+  const devicon = DeviconsNameList.find(
+    (devicon) => languageName.toLowerCase() === devicon.name
+  );
+
+  const icon = `devicon-${devicon?.name}-plain colored`;
+
+  return draggable ? (
+    <Draggable draggableId={buildpack.name} index={index} key={buildpack.name}>
+      {(provided) => (
+        <StyledCard
+          marginBottom="5px"
+          {...provided.draggableProps}
+          {...provided.dragHandleProps}
+          ref={provided.innerRef}
+          key={buildpack.name}
+        >
+          <ContentContainer>
+            <Icon disableMarginRight={devicon == null} className={icon} />
+            <EventInformation>
+              <EventName>{buildpack?.name}</EventName>
+            </EventInformation>
+          </ContentContainer>
+          <ActionContainer>
+            <ActionButton onClick={() => onClickFn(buildpack.buildpack)}>
+              <span className="material-icons">
+                {action === "remove" ? "delete" : "add"}
+              </span>
+            </ActionButton>
+          </ActionContainer>
+        </StyledCard>
+      )}
+    </Draggable>
+  ) : (
+    <StyledCard marginBottom="5px" key={buildpack.name}>
+      <ContentContainer>
+        <Icon disableMarginRight={devicon == null} className={icon} />
+        <EventInformation>
+          <EventName>{buildpack?.name}</EventName>
+        </EventInformation>
+      </ContentContainer>
+      <ActionContainer>
+        <ActionButton onClick={() => onClickFn(buildpack.buildpack)}>
+          <span className="material-icons">
+            {action === "remove" ? "delete" : "add"}
+          </span>
+        </ActionButton>
+      </ActionContainer>
+    </StyledCard>
+  );
+};
+
+export default BuildpackCard;
+
+const fadeIn = keyframes`
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+`;
+
+const StyledCard = styled.div<{ marginBottom?: string }>`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border: 1px solid #494b4f;
+  background: ${({ theme }) => theme.fg};
+  margin-bottom: ${(props) => props.marginBottom || "30px"};
+  border-radius: 8px;
+  padding: 14px;
+  overflow: hidden;
+  height: 60px;
+  font-size: 13px;
+  animation: ${fadeIn} 0.5s;
+`;
+
+const ContentContainer = styled.div`
+  display: flex;
+  height: 100%;
+  width: 100%;
+  align-items: center;
+`;
+
+const Icon = styled.span<{ disableMarginRight: boolean }>`
+  font-size: 20px;
+  margin-left: 10px;
+  ${(props) => {
+    if (!props.disableMarginRight) {
+      return "margin-right: 20px";
+    }
+  }}
+`;
+
+const EventInformation = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+  height: 100%;
+`;
+
+const EventName = styled.div`
+  font-family: "Work Sans", sans-serif;
+  font-weight: 500;
+  color: #ffffff;
+`;
+
+const ActionContainer = styled.div`
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+  height: 100%;
+`;
+
+const ActionButton = styled.button`
+  position: relative;
+  border: none;
+  background: none;
+  color: white;
+  padding: 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  cursor: pointer;
+  color: #aaaabb;
+
+  :hover {
+    background: #ffffff11;
+    border: 1px solid #ffffff44;
+  }
+
+  > span {
+    font-size: 20px;
+  }
+`;

+ 151 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/buildpacks/BuildpackConfigurationModal.tsx

@@ -0,0 +1,151 @@
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import React from "react";
+import BuildpackList from "./BuildpackList";
+import AddCustomBuildpackComponent from "./AddCustomBuildpack";
+import Icon from "components/porter/Icon";
+import Button from "components/porter/Button";
+import Modal from "components/porter/Modal";
+import styled from "styled-components";
+import Select from "components/porter/Select";
+import stars from "assets/stars-white.svg";
+import { Buildpack } from "main/home/app-dashboard/types/buildpack";
+import { Build, PorterAppFormData } from "../CreateApplication";
+import { Controller, useFieldArray, useFormContext } from "react-hook-form";
+
+type Props = {
+  build: Build;
+  closeModal: () => void;
+  sortedStackOptions: { value: string; label: string }[];
+  availableBuildpacks: Buildpack[];
+  setAvailableBuildpacks: (buildpacks: Buildpack[]) => void;
+  isDetectingBuildpacks: boolean;
+  detectBuildpacksError: string;
+  detectAndSetBuildPacks: () => void;
+};
+
+const BuildpackConfigurationModal: React.FC<Props> = ({
+  build,
+  closeModal,
+  sortedStackOptions,
+  availableBuildpacks,
+  setAvailableBuildpacks,
+  isDetectingBuildpacks,
+  detectBuildpacksError,
+  detectAndSetBuildPacks,
+}) => {
+  const { control } = useFormContext<PorterAppFormData>();
+  const { append } = useFieldArray({
+    control,
+    name: "build.buildpacks",
+  });
+
+  return (
+    <Modal closeModal={closeModal}>
+      <Text size={16}>Buildpack Configuration</Text>
+      <Spacer y={1} />
+      <Scrollable>
+        <Text>Builder:</Text>
+        {!build.builder && (
+          <>
+            <Spacer y={0.5} />
+            <Text color="helper">
+              No builder detected. Click 'Detect buildpacks' below to scan your
+              repository for available builders and buildpacks.
+            </Text>
+          </>
+        )}
+        {!!build.builder && (
+          <Controller
+            control={control}
+            name="build.builder"
+            render={({ field: { onChange } }) => (
+              <>
+                <Spacer y={0.5} />
+                <Select
+                  value={build.builder}
+                  width="300px"
+                  options={sortedStackOptions}
+                  setValue={(val) => {
+                    onChange(val);
+                  }}
+                />
+              </>
+            )}
+          />
+        )}
+        <BuildpackList
+          build={build}
+          availableBuildpacks={availableBuildpacks}
+          setAvailableBuildpacks={setAvailableBuildpacks}
+          showAvailableBuildpacks={true}
+          isDetectingBuildpacks={isDetectingBuildpacks}
+          detectBuildpacksError={detectBuildpacksError}
+          droppableId={"modal"}
+        />
+        <Spacer y={0.5} />
+        <Text>Custom buildpacks</Text>
+        <Spacer y={0.5} />
+        <Text color="helper">
+          You may also add buildpacks by directly providing their GitHub links
+          or links to ZIP files that contain the buildpack source code.
+        </Text>
+        <Spacer y={1} />
+        <AddCustomBuildpackComponent
+          onAdd={(bp) => {
+            append(bp);
+          }}
+        />
+        <Spacer y={2} />
+      </Scrollable>
+      <Footer>
+        <Shade />
+        <FooterButtons>
+          <Button onClick={() => detectAndSetBuildPacks()}>
+            <Icon src={stars} height="15px" />
+            <Spacer inline x={0.5} />
+            Detect buildpacks
+          </Button>
+          <Button onClick={closeModal} width={"75px"}>
+            Close
+          </Button>
+        </FooterButtons>
+      </Footer>
+    </Modal>
+  );
+};
+export default BuildpackConfigurationModal;
+
+const Scrollable = styled.div`
+  overflow-y: auto;
+  padding: 0 25px;
+  width: calc(100% + 50px);
+  margin-left: -25px;
+  max-height: calc(100vh - 300px);
+`;
+
+const FooterButtons = styled.div`
+  display: flex;
+  justify-content: space-between;
+`;
+
+const Footer = styled.div`
+  position: relative;
+  width: calc(100% + 50px);
+  margin-left: -25px;
+  padding: 0 25px;
+  border-bottom-left-radius: 10px;
+  border-bottom-right-radius: 10px;
+  background: ${({ theme }) => theme.fg};
+  margin-bottom: -30px;
+  padding-bottom: 30px;
+`;
+
+const Shade = styled.div`
+  position: absolute;
+  top: -50px;
+  left: 0;
+  height: 50px;
+  width: 100%;
+  background: linear-gradient(to bottom, #00000000, ${({ theme }) => theme.fg});
+`;

+ 146 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/buildpacks/BuildpackList.tsx

@@ -0,0 +1,146 @@
+import React from "react";
+import BuildpackCard from "./BuildpackCard";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+import Loading from "components/Loading";
+import Error from "components/porter/Error";
+import { Droppable, DragDropContext } from "react-beautiful-dnd";
+import { Buildpack } from "main/home/app-dashboard/types/buildpack";
+import { Build, PorterAppFormData } from "../CreateApplication";
+import { useFieldArray, useFormContext } from "react-hook-form";
+
+interface Props {
+  build: Build;
+  availableBuildpacks: Buildpack[];
+  setAvailableBuildpacks: (buildpacks: Buildpack[]) => void;
+  showAvailableBuildpacks: boolean;
+  isDetectingBuildpacks: boolean;
+  detectBuildpacksError: string;
+  droppableId: string;
+}
+const BuildpackList: React.FC<Props> = ({
+  build,
+  availableBuildpacks,
+  setAvailableBuildpacks,
+  showAvailableBuildpacks,
+  isDetectingBuildpacks,
+  detectBuildpacksError,
+  droppableId,
+}) => {
+  console.log("build", build);
+  const { control } = useFormContext<PorterAppFormData>();
+  const { remove, append, swap } = useFieldArray({
+    control,
+    name: "build.buildpacks",
+  });
+
+  const handleRemoveBuildpack = (buildpackToRemove: string) => {
+    const bpIdx = build.buildpacks.findIndex(
+      (bp) => bp.buildpack === buildpackToRemove
+    );
+    const buildpack = build.buildpacks[bpIdx];
+    if (bpIdx !== -1) {
+      remove(bpIdx);
+      if (buildpack) {
+        setAvailableBuildpacks([...availableBuildpacks, buildpack]);
+      }
+    }
+  };
+
+  const handleAddBuildpack = (buildpackToAdd: string) => {
+    const buildpackAdded = build.buildpacks.some(
+      (bp) => bp.buildpack === buildpackToAdd
+    );
+    const buildpack = availableBuildpacks.find(
+      (bp) => bp.buildpack === buildpackToAdd
+    );
+    if (!buildpackAdded && buildpack) {
+      append(buildpack);
+      setAvailableBuildpacks(
+        availableBuildpacks.filter((bp) => bp.buildpack !== buildpackToAdd)
+      );
+    }
+  };
+
+  const onDragEnd = (result: any) => {
+    if (!result.destination) {
+      return;
+    }
+
+    const items = Array.from(build.buildpacks);
+    const [reorderedItem] = items.splice(result.source.index, 1);
+    items.splice(result.destination.index, 0, reorderedItem);
+    swap(result.source.index, result.destination.index);
+  };
+
+  const renderAvailableBuildpacks = () => {
+    if (isDetectingBuildpacks) {
+      return <Loading />;
+    }
+
+    if (detectBuildpacksError) {
+      return <Error message={detectBuildpacksError} />;
+    }
+
+    if (availableBuildpacks.length > 0) {
+      return availableBuildpacks.map((buildpack, index) => {
+        return (
+          <BuildpackCard
+            buildpack={buildpack}
+            action={"add"}
+            onClickFn={handleAddBuildpack}
+            index={index}
+            draggable={false}
+            key={`${buildpack.name}-${index}-available`}
+          />
+        );
+      });
+    }
+
+    return <Text color="helper">No available buildpacks detected.</Text>;
+  };
+
+  return (
+    <DragDropContext onDragEnd={onDragEnd}>
+      {showAvailableBuildpacks && (
+        <>
+          <Spacer y={0.5} />
+          <Text>Selected buildpacks:</Text>
+          <Spacer y={0.5} />
+        </>
+      )}
+      {build.buildpacks.length !== 0 && (
+        <Droppable droppableId={droppableId}>
+          {(provided) => (
+            <div {...provided.droppableProps} ref={provided.innerRef}>
+              {build.buildpacks.map((buildpack, index) => (
+                <BuildpackCard
+                  buildpack={buildpack}
+                  action={"remove"}
+                  onClickFn={handleRemoveBuildpack}
+                  index={index}
+                  draggable={true}
+                  key={`${buildpack.name}-${index}-selected`}
+                />
+              ))}
+              {provided.placeholder}
+            </div>
+          )}
+        </Droppable>
+      )}
+      {build.buildpacks.length === 0 && (
+        <Text color="helper">No buildpacks selected.</Text>
+      )}
+      {showAvailableBuildpacks && (
+        <>
+          <Spacer y={0.5} />
+          <Text>Available buildpacks:</Text>
+          <Spacer y={0.5} />
+          {renderAvailableBuildpacks()}
+        </>
+      )}
+    </DragDropContext>
+  );
+};
+
+export default BuildpackList;

+ 247 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/buildpacks/BuildpackSettings.tsx

@@ -0,0 +1,247 @@
+import React, { useEffect, useMemo, useState } from "react";
+import styled, { keyframes } from "styled-components";
+import { Build, PorterAppFormData, SourceOptions } from "../CreateApplication";
+import Helper from "components/form-components/Helper";
+import Error from "components/porter/Error";
+import { useQuery } from "@tanstack/react-query";
+import api from "shared/api";
+import {
+  BUILDPACK_TO_NAME,
+  Buildpack,
+  DEFAULT_BUILDER_NAME,
+  DEFAULT_HEROKU_STACK,
+  DetectedBuildpack,
+  detectedBuildpackSchema,
+} from "main/home/app-dashboard/types/buildpack";
+import { z } from "zod";
+import Spacer from "components/porter/Spacer";
+import Button from "components/porter/Button";
+import BuildpackList from "./BuildpackList";
+import BuildpackConfigurationModal from "./BuildpackConfigurationModal";
+import { useFieldArray, useFormContext } from "react-hook-form";
+
+type Props = {
+  projectId: number;
+  build: Build;
+  source: SourceOptions & { type: "github" };
+  autoDetectionDisabled?: boolean;
+};
+
+const BuildpackSettings: React.FC<Props> = ({
+  projectId,
+  build,
+  source,
+  autoDetectionDisabled,
+}) => {
+  const [stackOptions, setStackOptions] = useState<
+    { label: string; value: string }[]
+  >([]);
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [availableBuildpacks, setAvailableBuildpacks] = useState<Buildpack[]>(
+    []
+  );
+  const { control, setValue } = useFormContext<PorterAppFormData>();
+  const { replace } = useFieldArray({
+    control,
+    name: "build.buildpacks",
+  });
+
+  const { data, status, refetch } = useQuery(
+    [
+      "detectBuildpacks",
+      projectId,
+      source.git_repo_name,
+      source.git_branch,
+      build.build_context,
+    ],
+    async () => {
+      const detectBuildPackRes = await api.detectBuildpack<DetectedBuildpack[]>(
+        "<token>",
+        {
+          dir: build.build_context || ".",
+        },
+        {
+          project_id: projectId,
+          git_repo_id: source.git_repo_id,
+          kind: "github",
+          owner: source.git_repo_name.split("/")[0],
+          name: source.git_repo_name.split("/")[1],
+          branch: source.git_branch,
+        }
+      );
+
+      console.log("detectBuildPackRes", detectBuildPackRes);
+
+      const detectedBuildpacks = z
+        .array(detectedBuildpackSchema)
+        .parseAsync(detectBuildPackRes.data);
+
+      return detectedBuildpacks;
+    },
+    {
+      enabled: !autoDetectionDisabled,
+    }
+  );
+
+  console.log("data", data);
+  console.log("status", status);
+
+  const errorMessage = useMemo(
+    () =>
+      status === "error"
+        ? `Unable to detect buildpacks at path: ${build.build_context}. Please make sure your repo, branch, and application root path are all set correctly and attempt to detect again.`
+        : "",
+    [build.build_context]
+  );
+
+  useEffect(() => {
+    if (autoDetectionDisabled) {
+      // in this case, we are not detecting buildpacks, so we just populate based on the DB
+      if (build.builder) {
+        setValue("build.builder", build.builder);
+        setStackOptions([{ label: build.builder, value: build.builder }]);
+      }
+      if (build.buildpacks.length) {
+        const bps = build.buildpacks.map((bp) => ({
+          ...bp,
+          name: BUILDPACK_TO_NAME[bp.buildpack] ?? bp,
+        }));
+        replace(bps);
+      }
+    } else {
+      if (!data) {
+        return;
+      }
+
+      if (data.length === 0) {
+        return;
+      }
+      setStackOptions(
+        data
+          .flatMap((builder) => {
+            return builder.builders.map((stack) => ({
+              label: `${builder.name} - ${stack}`,
+              value: stack.toLowerCase(),
+            }));
+          })
+          .sort((a, b) => {
+            if (a.label < b.label) {
+              return -1;
+            }
+            if (a.label > b.label) {
+              return 1;
+            }
+            return 0;
+          })
+      );
+
+      const defaultBuilder =
+        data.find(
+          (builder) => builder.name.toLowerCase() === DEFAULT_BUILDER_NAME
+        ) ?? data[0];
+
+      const allBuildpacks = defaultBuilder.others.concat(
+        defaultBuilder.detected
+      );
+
+      let detectedBuilder: string;
+      if (
+        defaultBuilder.builders.length &&
+        defaultBuilder.builders.includes(DEFAULT_HEROKU_STACK)
+      ) {
+        setValue("build.builder", DEFAULT_HEROKU_STACK);
+        detectedBuilder = DEFAULT_HEROKU_STACK;
+      } else {
+        setValue("build.builder", defaultBuilder.builders[0]);
+        detectedBuilder = defaultBuilder.builders[0];
+      }
+
+      if (!autoDetectionDisabled) {
+        setValue("build.builder", detectedBuilder);
+        replace(defaultBuilder.detected);
+        setAvailableBuildpacks(defaultBuilder.others);
+      } else {
+        setValue("build.builder", detectedBuilder);
+        setAvailableBuildpacks(
+          allBuildpacks.filter(
+            (bp) => !build.buildpacks.some((b) => b.buildpack === bp.buildpack)
+          )
+        );
+      }
+    }
+  }, [data]);
+
+  return (
+    <BuildpackConfigurationContainer>
+      {build.buildpacks.length > 0 && (
+        <>
+          <Helper>
+            The following buildpacks were automatically detected. You can also
+            manually add, remove, or re-order buildpacks here.
+          </Helper>
+          <BuildpackList
+            build={build}
+            availableBuildpacks={availableBuildpacks}
+            setAvailableBuildpacks={setAvailableBuildpacks}
+            showAvailableBuildpacks={false}
+            isDetectingBuildpacks={status === "loading"}
+            detectBuildpacksError={errorMessage}
+            droppableId={"non-modal"}
+          />
+        </>
+      )}
+      {!autoDetectionDisabled && status === "error" && (
+        <>
+          <Spacer y={1} />
+          <Error
+            message={`Unable to detect buildpacks at path: ${build.build_context}. Please make sure your repo, branch, and application root path are all set correctly and attempt to detect again.`}
+          />
+        </>
+      )}
+      <Spacer y={1} />
+      <Button
+        onClick={() => {
+          setIsModalOpen(true);
+        }}
+      >
+        <I className="material-icons">add</I> Add / detect buildpacks
+      </Button>
+      {isModalOpen && (
+        <BuildpackConfigurationModal
+          build={build}
+          closeModal={() => setIsModalOpen(false)}
+          sortedStackOptions={stackOptions}
+          availableBuildpacks={availableBuildpacks}
+          setAvailableBuildpacks={setAvailableBuildpacks}
+          isDetectingBuildpacks={status === "loading"}
+          detectBuildpacksError={errorMessage}
+          detectAndSetBuildPacks={refetch}
+        />
+      )}
+    </BuildpackConfigurationContainer>
+  );
+};
+
+export default BuildpackSettings;
+
+const I = styled.i`
+  color: white;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  margin-right: 5px;
+  justify-content: center;
+`;
+
+const fadeIn = keyframes`
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+`;
+
+const BuildpackConfigurationContainer = styled.div`
+  animation: ${fadeIn} 0.75s;
+`;

+ 235 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/create-application/services/ServiceList.tsx

@@ -0,0 +1,235 @@
+import React, { useContext, useEffect, useState } from "react";
+import ServiceContainer from "./ServiceContainer";
+import styled from "styled-components";
+import Spacer from "components/porter/Spacer";
+import Modal from "components/porter/Modal";
+import Text from "components/porter/Text";
+import Select from "components/porter/Select";
+import Input from "components/porter/Input";
+import Container from "components/porter/Container";
+import Button from "components/porter/Button";
+
+import web from "assets/web.png";
+import worker from "assets/worker.png";
+import job from "assets/job.png";
+import { Service, ServiceType } from "./serviceTypes";
+
+type ServiceListProps = {
+  services: Service[];
+  setServices: (services: Service[]) => void;
+  addNewText: string;
+  defaultExpanded?: boolean;
+  chart?: any;
+  limitOne?: boolean;
+  prePopulateService?: Service;
+  setExpandedJob?: (x: string) => void;
+};
+
+const ServiceList: React.FC<ServiceListProps> = ({
+  services,
+  setServices,
+  addNewText,
+  chart,
+  limitOne = false,
+  setExpandedJob,
+  prePopulateService,
+}) => {
+  const [showAddServiceModal, setShowAddServiceModal] = useState<boolean>(
+    false
+  );
+  const [serviceName, setServiceName] = useState<string>("");
+  const [serviceType, setServiceType] = useState<ServiceType>("web");
+  const isServiceNameValid = (name: string) => {
+    const regex = /^[a-z0-9-]+$/;
+
+    return regex.test(name);
+  };
+  const isServiceNameDuplicate = (name: string) => {
+    const serviceNames = services.map((service) => service.name);
+    return serviceNames.includes(name);
+  };
+
+  const maybeGetError = (): string | undefined => {
+    if (serviceName.length > 30) {
+      return "Must be 30 characters or less.";
+    } else if (serviceName != "" && !isServiceNameValid(serviceName)) {
+      return "Lowercase letters, numbers, and '-' only.";
+    } else if (isServiceNameDuplicate(serviceName)) {
+      return "Service name is duplicate";
+    } else {
+      return undefined;
+    }
+  };
+
+  const maybeRenderAddServicesButton = () => {
+    if (limitOne && services.length > 0) {
+      return null;
+    }
+    return (
+      <>
+        <AddServiceButton
+          onClick={() => {
+            if (prePopulateService == null) {
+              setShowAddServiceModal(true);
+              setServiceType("web");
+            } else {
+              const newServices = [...services, prePopulateService];
+              setServices(newServices);
+            }
+          }}
+        >
+          <i className="material-icons add-icon">add_icon</i>
+          {addNewText}
+        </AddServiceButton>
+        <Spacer y={0.5} />
+      </>
+    );
+  };
+
+  return (
+    <>
+      {services.length > 0 && (
+        <ServicesContainer>
+          {services.map((service, index) => {
+            return (
+              <ServiceContainer
+                key={service.name}
+                setExpandedJob={setExpandedJob}
+                service={service}
+                chart={chart}
+                editService={(newService: Service) => {
+                  const newServices = services.map((s, i) =>
+                    i === index ? newService : s
+                  );
+                  setServices(newServices);
+                }}
+                deleteService={() => {
+                  const newServices = services.filter((_, i) => i !== index);
+                  setServices(newServices);
+                }}
+              />
+            );
+          })}
+        </ServicesContainer>
+      )}
+      {maybeRenderAddServicesButton()}
+      {showAddServiceModal && (
+        <Modal closeModal={() => setShowAddServiceModal(false)} width="500px">
+          <Text size={16}>{addNewText}</Text>
+          <Spacer y={1} />
+          <Text color="helper">Select a service type:</Text>
+          <Spacer y={0.5} />
+          <Container row>
+            <ServiceIcon>
+              {serviceType === "web" && <img src={web} />}
+              {serviceType === "worker" && <img src={worker} />}
+              {serviceType === "job" && <img src={job} />}
+            </ServiceIcon>
+            <Select
+              value={serviceType}
+              width="100%"
+              setValue={(value: string) => setServiceType(value as ServiceType)}
+              options={[
+                { label: "Web", value: "web" },
+                { label: "Worker", value: "worker" },
+                { label: "Cron Job", value: "job" },
+              ]}
+            />
+          </Container>
+          <Spacer y={1} />
+          <Text color="helper">Name this service:</Text>
+          <Spacer y={0.5} />
+          <Input
+            placeholder="ex: my-service"
+            width="100%"
+            value={serviceName}
+            error={maybeGetError()}
+            setValue={setServiceName}
+          />
+          <Spacer y={1} />
+          <Button
+            onClick={() => {
+              const newServices = [
+                ...services,
+                Service.default(serviceName, serviceType),
+              ];
+              setServices(newServices);
+              setShowAddServiceModal(false);
+              setServiceName("");
+              setServiceType("web");
+            }}
+            disabled={
+              !isServiceNameValid(serviceName) ||
+              isServiceNameDuplicate(serviceName) ||
+              serviceName?.length > 61
+            }
+          >
+            <I className="material-icons">add</I> Add service
+          </Button>
+        </Modal>
+      )}
+    </>
+  );
+};
+
+export default ServiceList;
+
+const ServiceIcon = styled.div`
+  border: 1px solid #494b4f;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 35px;
+  width: 35px;
+  min-width: 35px;
+  margin-right: 10px;
+  overflow: hidden;
+  border-radius: 5px;
+  > img {
+    height: 18px;
+    animation: floatIn 0.5s 0s;
+    @keyframes floatIn {
+      from {
+        opacity: 0;
+        transform: translateY(7px);
+      }
+      to {
+        opacity: 1;
+        transform: translateY(0px);
+      }
+    }
+  }
+`;
+
+const I = styled.i`
+  color: white;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  margin-right: 7px;
+  justify-content: center;
+`;
+
+const ServicesContainer = styled.div``;
+
+const AddServiceButton = styled.div`
+  color: #aaaabb;
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+    color: white;
+  }
+  display: flex;
+  align-items: center;
+  border-radius: 5px;
+  height: 40px;
+  font-size: 13px;
+  width: 100%;
+  padding-left: 10px;
+  cursor: pointer;
+  .add-icon {
+    width: 30px;
+    font-size: 20px;
+  }
+`;

+ 31 - 27
dashboard/src/main/home/app-dashboard/types/buildpack.ts

@@ -1,32 +1,36 @@
-export type BuildConfig = {
-    builder: string;
-    buildpacks: string[];
-    config: null | {
-        [key: string]: string;
-    };
-};
-export type Buildpack = {
-    name: string;
-    buildpack: string;
-    config: {
-        [key: string]: string;
-    };
-};
-export type DetectedBuildpack = {
-    name: string;
-    builders: string[];
-    detected: Buildpack[];
-    others: Buildpack[];
-    buildConfig: BuildConfig;
-};
+import { z } from "zod";
+
+export const buildConfigSchema = z.object({
+  builder: z.string(),
+  buildpacks: z.array(z.string()),
+  config: z.record(z.any()).optional(),
+});
+export type BuildConfig = z.infer<typeof buildConfigSchema>;
+
+export const buildpackSchema = z.object({
+  name: z.string(),
+  buildpack: z.string(),
+  config: z.record(z.any()).nullish(),
+});
+export type Buildpack = z.infer<typeof buildpackSchema>;
+
+export const detectedBuildpackSchema = z.object({
+  name: z.string(),
+  builders: z.array(z.string()),
+  detected: z.array(buildpackSchema),
+  others: z.array(buildpackSchema),
+  buildConfig: buildConfigSchema.optional(),
+});
+export type DetectedBuildpack = z.infer<typeof detectedBuildpackSchema>;
+
 export const DEFAULT_BUILDER_NAME = "heroku";
 export const DEFAULT_PAKETO_STACK = "paketobuildpacks/builder:full";
 export const DEFAULT_HEROKU_STACK = "heroku/buildpacks:20";
 
 export const BUILDPACK_TO_NAME: { [key: string]: string } = {
-    "heroku/nodejs": "NodeJS",
-    "heroku/python": "Python",
-    "heroku/java": "Java",
-    "heroku/ruby": "Ruby",
-    "heroku/go": "Go",
-};
+  "heroku/nodejs": "NodeJS",
+  "heroku/python": "Python",
+  "heroku/java": "Java",
+  "heroku/ruby": "Ruby",
+  "heroku/go": "Go",
+};

+ 4 - 2
go.mod

@@ -52,7 +52,7 @@ require (
 	google.golang.org/api v0.103.0
 	google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd
 	google.golang.org/grpc v1.52.0
-	google.golang.org/protobuf v1.30.0
+	google.golang.org/protobuf v1.31.0
 	gorm.io/gorm v1.24.2
 	k8s.io/api v0.26.0
 	k8s.io/apimachinery v0.26.0
@@ -216,7 +216,7 @@ require (
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect
 	github.com/gdamore/tcell/v2 v2.5.1 // indirect
-	github.com/ghodss/yaml v1.0.0 // indirect
+	github.com/ghodss/yaml v1.0.0
 	github.com/go-errors/errors v1.4.2 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
@@ -352,3 +352,5 @@ require (
 	sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
 )
+
+replace github.com/porter-dev/api-contracts => ../api-contracts

+ 2 - 4
go.sum

@@ -1494,8 +1494,6 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.0.75 h1:Koje0VJLlQ1Nxw25N4IcaK1nqDlPpktAM5Jzrkd0yRA=
-github.com/porter-dev/api-contracts v0.0.75/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -2550,8 +2548,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 1 - 270
go.work.sum

@@ -113,6 +113,7 @@ cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqVi
 cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A=
 cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ=
 cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=
+connectrpc.com/connect v1.11.0 h1:Av2KQXxSaX4vjqhf5Cl01SX4dqYADQ38eBtr84JSUBk=
 github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
 github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -140,294 +141,24 @@ github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4=
 github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
 github.com/porter-dev/api-contracts v0.0.63/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
 github.com/porter-dev/api-contracts v0.0.75/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
-github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
-github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
-github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
-github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
-github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/riandyrn/otelchi v0.5.1/go.mod h1:ZxVxNEl+jQ9uHseRYIxKWRb3OY8YXFEu+EkNiiSNUEA=
-github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
-github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo=
-github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
-github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
-github.com/santhosh-tekuri/jsonschema/v5 v5.0.1/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
-github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
-github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
-github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
-github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
-github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
-github.com/sendgrid/rest v2.6.3+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
-github.com/sendgrid/sendgrid-go v3.8.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
-github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/sethvargo/go-envconfig v0.8.2/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0=
-github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA=
-github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
-github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
-github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
-github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
-github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
-github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
-github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
 github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
-github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
-github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
-github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
-github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
-github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
-github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
-github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
-github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck=
-github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
-github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
-github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
-github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
-github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
-github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
-github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
-github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
-github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
-github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
-github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
-github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
-github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
-github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
-github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
-github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
-github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
-github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
-github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
-github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
-github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
-github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
-github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
-github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
-github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
-github.com/xanzy/go-gitlab v0.68.0/go.mod h1:o4yExCtdaqlM8YGdDJWuZoBmfxBsmA9TPEjs9mx1UO4=
-github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
-github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
-github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
-github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
-github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
-github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
-github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
-github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
-github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
-github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
-github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
-go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
-go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
-go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
-go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
-go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/contrib v0.20.0 h1:ubFQUn0VCZ0gPwIoJfBJVpeBlyRMxu8Mm/huKWYd9p0=
-go.opentelemetry.io/contrib v1.0.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM=
-go.opentelemetry.io/contrib/instrumentation/host v0.40.0/go.mod h1:7HKwySOL83pJ4FP3SJnjA8cOK4MxE2qHtq9ErpNqTks=
-go.opentelemetry.io/contrib/instrumentation/runtime v0.40.0/go.mod h1:zmll4G8j5zRZeFURG6t/N7SOl7M5kUHQfV5UVqTaQFI=
-go.opentelemetry.io/contrib/propagators/b3 v1.13.0/go.mod h1:zy2hz1TpGUoJzSwlBchVGvVAFQS8s2pglKLbrAFZ+Sc=
-go.opentelemetry.io/contrib/propagators/ot v1.13.0/go.mod h1:R6Op9T6LxNaMRVlGD0wVwz40LSsAq296CXiEydKLQBU=
-go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
-go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
 go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg=
-go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.12.0/go.mod h1:rqbht/LlhVBgn5+k3M5QK96K5Xb0DvXpMJ5SFQpY6uw=
-go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.35.0/go.mod h1:HKkSo2BOMO2CUdoIUuc/e4aLeMbeZaj+gNgjBj/Qdzk=
-go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.35.0/go.mod h1:BTWTNRCV2jdeEaKP+QJWD9g86QnFzxhZfsQZ1w7cSx4=
-go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.35.0/go.mod h1:RC2duYQUmta07UZKXeqL5yehj4CdDkyhcMLf3fYnMek=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.12.0/go.mod h1:IlaGLENJkAl9+Xoo3J0unkdOwtL+rmqZ3ryMjUtYA94=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.12.0/go.mod h1:jSqjV+Knu1Jyvh+l3fx7V210Ev3HHgNQAi8YqpXaQP8=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.12.0/go.mod h1:C+onYX2j5QH653b3wGJwowYr8jLMjBJw35QcaCQQK0U=
-go.opentelemetry.io/otel/metric v0.37.0/go.mod h1:DmdaHfGt54iV6UKxsV9slj2bBRJcKC1B1uvDLIioc1s=
-go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
-go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM=
-go.opentelemetry.io/otel/sdk/metric v0.37.0/go.mod h1:mO2WV1AZKKwhwHTV3AKOoIEb9LbUaENZDuGUQd+j4A0=
-go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
-go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
-go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
-go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
-go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
-go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
-golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
-golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
-golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
-golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
-google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
-google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
-google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
-google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
-google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
-google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
-gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
-gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
-gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
-gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
-gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
-gopkg.in/segmentio/analytics-go.v3 v3.1.0/go.mod h1:4QqqlTlSSpVlWA9/9nDcPw+FkM2yv1NQoYjUbL9/JAw=
-gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
-gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
-gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
-gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 helm.sh/helm/v3 v3.7.1 h1:kED/HWx09QHHSJhYaJY6ttj/BhmzBmT1oupKslncibY=
-istio.io/api v0.0.0-20221109202042-b9e5d446a83d/go.mod h1:hQkF0Q19MCmfOTre/Sg4KvrwwETq45oaFplnBm2p4j8=
-istio.io/client-go v1.16.0/go.mod h1:UV8SFeM2qNime5sobkr2m8oTCPxxVt9xCY4ol50U9YQ=
-k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
-k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
-k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
-k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
-k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
-k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
-k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
-k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
-k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
-k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
-k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
-k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
-k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0=
-k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
-k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
-k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
-k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
-k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
-k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
-k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=
 k8s.io/cri-api v0.23.1 h1:0DHL/hpTf4Fp+QkUXFefWcp1fhjXr9OlNdY9X99c+O8=
-k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
-k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
-k8s.io/helm v2.17.0+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI=
-k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
-k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
-k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
-k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
-k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
-sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
-sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
-sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
-sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=