Просмотр исходного кода

Merge branch 'simplified-view' of github.com:porter-dev/porter into simplified-view

Justin Rhee 3 лет назад
Родитель
Сommit
0dac2a6364

+ 39 - 0
api/server/handlers/stacks/create.go

@@ -12,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/stefanmcshane/helm/pkg/chart"
 )
 
 type CreateStackHandler struct {
@@ -95,3 +96,41 @@ func (c *CreateStackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 	w.WriteHeader(http.StatusCreated)
 }
+
+func createChartFromDependencies(deps []types.Dependency) (*chart.Chart, error) {
+	metadata := &chart.Metadata{
+		Name:        "umbrella",
+		Description: "Web application that is exposed to external traffic.",
+		Version:     "0.96.0",
+		APIVersion:  "v2",
+		Home:        "https://getporter.dev/",
+		Icon:        "https://user-images.githubusercontent.com/65516095/111255214-07d3da80-85ed-11eb-99e2-fddcbdb99bdb.png",
+		Keywords: []string{
+			"porter",
+			"application",
+			"service",
+			"umbrella",
+		},
+		Type:         "application",
+		Dependencies: createChartDependencies(deps),
+	}
+
+	// create a new chart object with the metadata
+	c := &chart.Chart{
+		Metadata: metadata,
+	}
+	return c, nil
+}
+
+func createChartDependencies(deps []types.Dependency) []*chart.Dependency {
+	var chartDependencies []*chart.Dependency
+	for _, d := range deps {
+		chartDependencies = append(chartDependencies, &chart.Dependency{
+			Name:       d.Name,
+			Alias:      d.Alias,
+			Version:    d.Version,
+			Repository: d.Repository,
+		})
+	}
+	return chartDependencies
+}

+ 40 - 20
api/server/handlers/stacks/create_secret_and_open_pr.go

@@ -11,8 +11,8 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
@@ -34,25 +34,21 @@ func NewOpenStackPRHandler(
 }
 
 func (c *OpenStackPRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	gaid := c.Config().GithubAppConf.AppID
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-
-	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
-	if !ok {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("unable to get github owner and name params")))
+	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
 		return
 	}
 
-	// create the environment
-	request := &types.CreateSecretAndOpenGitHubPullRequest{}
-
+	request := &types.CreateSecretAndOpenGHPRRequest{}
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
 		return
 	}
 
-	client, err := getGithubClient(c.Config(), gaid)
+	client, err := getGithubClient(c.Config(), request.GithubAppInstallationID)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -70,16 +66,32 @@ func (c *OpenStackPRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	// create porter secret
+	secretName := fmt.Sprintf("PORTER_STACK_%d_%d", project.ID, cluster.ID)
+	err = actions.CreateGithubSecret(
+		client,
+		secretName,
+		encoded,
+		request.GithubRepoOwner,
+		request.GithubRepoName,
+	)
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error generating secret: %w", err)))
+		return
+	}
+
+	var pr *github.PullRequest
 	if request.OpenPr {
-		err = actions.OpenGithubPR(&actions.GithubPROpts{
-			Client:       client,
-			GitRepoOwner: owner,
-			GitRepoName:  name,
-			StackName:    request.StackName,
-			ProjectID:    project.ID,
-			ClusterID:    cluster.ID,
-			PorterToken:  encoded,
-			ServerURL:    c.Config().ServerConf.ServerURL,
+		pr, err = actions.OpenGithubPR(&actions.GithubPROpts{
+			Client:        client,
+			GitRepoOwner:  request.GithubRepoOwner,
+			GitRepoName:   request.GithubRepoName,
+			StackName:     stackName,
+			ProjectID:     project.ID,
+			ClusterID:     cluster.ID,
+			ServerURL:     c.Config().ServerConf.ServerURL,
+			DefaultBranch: request.Branch,
+			SecretName:    secretName,
 		})
 	}
 
@@ -93,13 +105,21 @@ func (c *OpenStackPRHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed))
 			}
 		} else {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error setting up preview environment in the github "+
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error setting up application in the github "+
 				"repo: %w", err)))
 			return
 		}
 	}
 
+	var resp types.CreateSecretAndOpenGHPRResponse
+	if pr != nil {
+		resp = types.CreateSecretAndOpenGHPRResponse{
+			URL: pr.GetHTMLURL(),
+		}
+	}
+
 	w.WriteHeader(http.StatusCreated)
+	c.WriteResult(w, r, resp)
 }
 
 func getGithubClient(config *config.Config, gitInstallationId int64) (*github.Client, error) {

+ 0 - 39
api/server/handlers/stacks/update.go

@@ -12,7 +12,6 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/stefanmcshane/helm/pkg/chart"
 )
 
 type UpdateStackHandler struct {
@@ -82,41 +81,3 @@ func (c *UpdateStackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	}
 	w.WriteHeader(http.StatusCreated)
 }
-
-func createChartFromDependencies(deps []types.Dependency) (*chart.Chart, error) {
-	metadata := &chart.Metadata{
-		Name:        "umbrella",
-		Description: "Web application that is exposed to external traffic.",
-		Version:     "0.96.0",
-		APIVersion:  "v2",
-		Home:        "https://getporter.dev/",
-		Icon:        "https://user-images.githubusercontent.com/65516095/111255214-07d3da80-85ed-11eb-99e2-fddcbdb99bdb.png",
-		Keywords: []string{
-			"porter",
-			"application",
-			"service",
-			"umbrella",
-		},
-		Type:         "application",
-		Dependencies: createChartDependencies(deps),
-	}
-
-	// create a new chart object with the metadata
-	c := &chart.Chart{
-		Metadata: metadata,
-	}
-	return c, nil
-}
-
-func createChartDependencies(deps []types.Dependency) []*chart.Dependency {
-	var chartDependencies []*chart.Dependency
-	for _, d := range deps {
-		chartDependencies = append(chartDependencies, &chart.Dependency{
-			Name:       d.Name,
-			Alias:      d.Alias,
-			Version:    d.Version,
-			Repository: d.Repository,
-		})
-	}
-	return chartDependencies
-}

+ 3 - 1
api/server/router/stack.go

@@ -1,6 +1,8 @@
 package router
 
 import (
+	"fmt"
+
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/stacks"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -147,7 +149,7 @@ func getStackRoutes(
 			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: relPath + "/{stack}/pr",
+				RelativePath: fmt.Sprintf("%s/{%s}/pr", relPath, types.URLParamStackName),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,

+ 4 - 1
api/server/shared/config/env/envconfs.go

@@ -46,7 +46,10 @@ type ServerConf struct {
 	GithubAppWebhookSecret string `env:"GITHUB_APP_WEBHOOK_SECRET"`
 	GithubAppID            string `env:"GITHUB_APP_ID"`
 	GithubAppSecretPath    string `env:"GITHUB_APP_SECRET_PATH"`
-	GithubAppSecret        []byte
+	// GithubAppSecretBase64 is a base64 encoded version of the GithubAppSecret. This can be used instead of GithubAppSecretPath to pass in a key, allowing for support in systems where mounting the secret is not possible.
+	// If GithubAppSecretBase64 is set, it will check for a file at GithubAppSecretPath. If a file is found, the file will NOT be overwritten. If no file it found, then GithubAppSecretBase64 will be decoded and written to GithubAppSecretPath.
+	GithubAppSecretBase64 string `env:"GITHUB_APP_SECRET_BASE64"`
+	GithubAppSecret       []byte
 
 	GoogleClientID         string `env:"GOOGLE_CLIENT_ID"`
 	GoogleClientSecret     string `env:"GOOGLE_CLIENT_SECRET"`

+ 39 - 8
api/server/shared/config/loader/loader.go

@@ -1,8 +1,10 @@
 package loader
 
 import (
+	"encoding/base64"
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -184,22 +186,51 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		res.Logger.Info().Msg("Created Github client")
 	}
 
+	if sc.GithubAppSecretBase64 != "" {
+		if sc.GithubAppSecretPath == "" {
+			sc.GithubAppSecretPath = "github-app-secret-key"
+		}
+		_, err := os.Stat(sc.GithubAppSecretPath)
+		if err != nil {
+			if !errors.Is(err, os.ErrNotExist) {
+				return nil, fmt.Errorf("GITHUB_APP_SECRET_BASE64 provided, but error checking if GITHUB_APP_SECRET_PATH exists: %w", err)
+			}
+			secret, err := base64.StdEncoding.DecodeString(sc.GithubAppSecretBase64)
+			if err != nil {
+				return nil, fmt.Errorf("GITHUB_APP_SECRET_BASE64 provided, but error decoding: %w", err)
+			}
+			_, err = createDirectoryRecursively(sc.GithubAppSecretPath)
+			if err != nil {
+				return nil, fmt.Errorf("GITHUB_APP_SECRET_BASE64 provided, but error creating directory for GITHUB_APP_SECRET_PATH: %w", err)
+			}
+			err = os.WriteFile(sc.GithubAppSecretPath, secret, os.ModePerm)
+			if err != nil {
+				return nil, fmt.Errorf("GITHUB_APP_SECRET_BASE64 provided, but error writing to GITHUB_APP_SECRET_PATH: %w", err)
+			}
+		}
+	}
+
 	if sc.GithubAppClientID != "" &&
 		sc.GithubAppClientSecret != "" &&
 		sc.GithubAppName != "" &&
 		sc.GithubAppWebhookSecret != "" &&
 		sc.GithubAppSecretPath != "" &&
 		sc.GithubAppID != "" {
-		AppID, err := strconv.Atoi(sc.GithubAppID)
+		if AppID, err := strconv.ParseInt(sc.GithubAppID, 10, 64); err == nil {
+			res.GithubAppConf = oauth.NewGithubAppClient(&oauth.Config{
+				ClientID:     sc.GithubAppClientID,
+				ClientSecret: sc.GithubAppClientSecret,
+				Scopes:       []string{"read:user"},
+				BaseURL:      sc.ServerURL,
+			}, sc.GithubAppName, sc.GithubAppWebhookSecret, sc.GithubAppSecretPath, AppID)
+		}
+
+		secret, err := ioutil.ReadFile(sc.GithubAppSecretPath)
 		if err != nil {
-			return nil, fmt.Errorf("could not read github App ID: %s", err)
+			return nil, fmt.Errorf("could not read github app secret: %s", err)
 		}
-		res.GithubAppConf = oauth.NewGithubAppClient(&oauth.Config{
-			ClientID:     sc.GithubAppClientID,
-			ClientSecret: sc.GithubAppClientSecret,
-			Scopes:       []string{"read:user"},
-			BaseURL:      sc.ServerURL,
-		}, sc.GithubAppName, sc.GithubAppWebhookSecret, sc.GithubAppSecretPath, int64(AppID))
+
+		sc.GithubAppSecret = append(sc.GithubAppSecret, secret...)
 	}
 
 	if sc.SlackClientID != "" && sc.SlackClientSecret != "" {

+ 1 - 0
api/types/request.go

@@ -48,6 +48,7 @@ const (
 	URLParamWildcard              URLParam = "*"
 	URLParamIntegrationID         URLParam = "integration_id"
 	URLParamAPIContractRevisionID URLParam = "contract_revision_id"
+	URLParamStackName             URLParam = "stack_name"
 )
 
 type Path struct {

+ 11 - 4
api/types/stack.go

@@ -4,7 +4,7 @@ type CreateStackReleaseRequest struct {
 	// The Helm values for this release
 	Values map[string]interface{} `json:"values"`
 	// Used to construct the Chart.yaml
-	Dependencies []Dependency `json:"dependencies" form:"required"`
+	Dependencies []Dependency `json:"dependencies"`
 	StackName    string       `json:"stack_name" form:"required,dns1123"`
 }
 
@@ -15,7 +15,14 @@ type Dependency struct {
 	Repository string `json:"repository" form:"required"`
 }
 
-type CreateSecretAndOpenGitHubPullRequest struct {
-	OpenPr    bool `json:"open_pr"`
-	StackName string
+type CreateSecretAndOpenGHPRRequest struct {
+	GithubAppInstallationID int64  `json:"github_app_installation_id" form:"required"`
+	GithubRepoOwner         string `json:"github_repo_owner" form:"required"`
+	GithubRepoName          string `json:"github_repo_name" form:"required"`
+	OpenPr                  bool   `json:"open_pr"`
+	Branch                  string `json:"branch"`
+}
+
+type CreateSecretAndOpenGHPRResponse struct {
+	URL string `json:"url"`
 }

+ 60 - 5
dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx

@@ -1,5 +1,5 @@
 import Modal from "components/porter/Modal";
-import React from "react";
+import React, { useContext } from "react";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import ExpandableSection from "components/porter/ExpandableSection";
@@ -8,14 +8,67 @@ import styled from "styled-components";
 import Button from "components/porter/Button";
 import Input from "components/porter/Input";
 import Select from "components/porter/Select";
+import api from "shared/api";
+import { Context } from "shared/Context";
 
 interface GithubActionModalProps {
     closeModal: () => void;
+    githubAppInstallationID?: number;
+    githubRepoOwner?: string;
+    githubRepoName?: string;
+    branch?: string;
+    stackName?: string;
+    projectId?: number;
+    clusterId?: number;
 }
 
+type Choice = "open_pr" | "copy";
+
 const GithubActionModal: React.FC<GithubActionModalProps> = ({
     closeModal,
+    githubAppInstallationID,
+    githubRepoOwner,
+    githubRepoName,
+    branch,
+    stackName,
+    projectId,
+    clusterId,
 }) => {
+    const [choice, setChoice] = React.useState<Choice>("open_pr");
+    const [loading, setLoading] = React.useState<boolean>(false);
+    const { currentProject, currentCluster } = useContext(Context);
+
+    const submit = async () => {
+        if (githubAppInstallationID && githubRepoOwner && githubRepoName && branch && stackName) {
+            try {
+                setLoading(true)
+                const res = await api.createSecretAndOpenGitHubPullRequest(
+                    "<token>",
+                    {
+                        github_app_installation_id: githubAppInstallationID,
+                        github_repo_owner: githubRepoOwner,
+                        github_repo_name: githubRepoName,
+                        branch,
+                        open_pr: choice === "open_pr",
+                    },
+                    {
+                        project_id: projectId,
+                        cluster_id: clusterId,
+                        stack_name: stackName,
+                    }
+                );
+                if (res?.data?.url) {
+                    window.open(res.data.url, "_blank", "noreferrer")
+                }
+            } catch (error) {
+                console.log(error)
+            } finally {
+                setLoading(false)
+            }
+        } else {
+            console.log("missing information")
+        }
+    }
     return (
         <Modal closeModal={closeModal}>
             <Text size={16}>
@@ -58,15 +111,17 @@ const GithubActionModal: React.FC<GithubActionModalProps> = ({
             <Spacer y={1} />
             <Select
                 options={[
-                    { label: "I authorize Porter to open a PR on my behalf", value: "I authorize Porter to open a PR on my behalf" },
-                    { label: "I will copy the file into my repository myself", value: "I will copy the file into my repository myself" },
+                    { label: "I authorize Porter to open a PR on my behalf", value: "open_pr" },
+                    { label: "I will copy the file into my repository myself", value: "copy" },
                 ]}
-                onChange={(x: any) => console.log(x)}
+                onChange={(x: Choice) => setChoice(x)}
                 width="100%"
             />
             <Button
-                onClick={closeModal}
+                onClick={submit}
                 width={"100%"}
+                status={loading ? "loading" : undefined}
+                loadingText="Opening PR..."
             >
                 Complete
             </Button>

+ 25 - 8
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -29,11 +29,11 @@ import EnvGroupArray, {
 } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 import Select from "components/porter/Select";
 import GithubActionModal from "./GithubActionModal";
-import { ActionConfigType, FullActionConfigType } from "shared/types";
+import { ActionConfigType, FullActionConfigType, FullGithubActionConfigType, GithubActionConfigType } from "shared/types";
 
 type Props = RouteComponentProps & {};
 
-const defaultActionConfig: ActionConfigType = {
+const defaultActionConfig: GithubActionConfigType = {
   git_repo: "",
   image_repo_uri: "",
   git_branch: "",
@@ -46,6 +46,7 @@ interface FormState {
   selectedSourceType: SourceType | undefined;
   serviceList: any[];
   envVariables: KeyValueType[];
+  releaseCommand: string;
 }
 
 const INITIAL_STATE: FormState = {
@@ -53,6 +54,7 @@ const INITIAL_STATE: FormState = {
   selectedSourceType: undefined,
   serviceList: [],
   envVariables: [],
+  releaseCommand: "",
 };
 
 const Validators: {
@@ -62,6 +64,7 @@ const Validators: {
   selectedSourceType: (value: SourceType | undefined) => value !== undefined,
   serviceList: (value: any[]) => value.length > 0,
   envVariables: (value: KeyValueType[]) => true,
+  releaseCommand: (value: string) => true,
 };
 
 const NewAppFlow: React.FC<Props> = ({ ...props }) => {
@@ -73,7 +76,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [isLoading, setIsLoading] = useState<boolean>(true);
   const [currentStep, setCurrentStep] = useState<number>(0);
   const [formState, setFormState] = useState<FormState>(INITIAL_STATE);
-  const [actionConfig, setActionConfig] = useState<ActionConfigType>({
+  const [actionConfig, setActionConfig] = useState<GithubActionConfigType>({
     ...defaultActionConfig,
   });
   const [procfileProcess, setProcfileProcess] = useState("");
@@ -85,7 +88,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [selectedRegistry, setSelectedRegistry] = useState(null);
   const [shouldCreateWorkflow, setShouldCreateWorkflow] = useState(true);
   const [buildConfig, setBuildConfig] = useState();
-  const getFullActionConfig = (): FullActionConfigType => {
+  const getFullActionConfig = (): FullGithubActionConfigType => {
     let imageRepoURI = `${selectedRegistry?.url}/${templateName}`;
     return {
       kind: "github",
@@ -197,7 +200,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                 <Text color="helper">
                   Lowercase letters, numbers, and "-" only.
                 </Text>
-                <Spacer y={0.5}></Spacer> 
+                <Spacer y={0.5}></Spacer>
                 <Input
                   placeholder="ex: academic-sophon"
                   value={formState.applicationName}
@@ -290,9 +293,14 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                 <Spacer y={0.5} />
                 <Input
                   placeholder="yarn ./scripts/run-migrations.js"
-                  value={""}
+                  value={formState.releaseCommand}
                   width="300px"
-                  setValue={(e) => {}}
+                  setValue={(e) => {
+                    setFormState({ ...formState, releaseCommand: e });
+                    if (Validators.releaseCommand(e)) {
+                      setCurrentStep(Math.max(currentStep, 6));
+                    }
+                  }}
                 />
               </>,
               */
@@ -311,7 +319,16 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         </StyledConfigureTemplate>
       </Div>
       {showGHAModal && (
-        <GithubActionModal closeModal={() => setShowGHAModal(false)} />
+        <GithubActionModal
+          closeModal={() => setShowGHAModal(false)}
+          githubAppInstallationID={actionConfig.git_repo_id}
+          githubRepoOwner={actionConfig.git_repo.split("/")[0]}
+          githubRepoName={actionConfig.git_repo.split("/")[1]}
+          branch={branch}
+          stackName={formState.applicationName}
+          projectId={currentProject.id}
+          clusterId={currentCluster.id}
+        />
       )}
     </CenterWrapper>
   );

+ 10 - 6
dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx

@@ -4,6 +4,7 @@ import DashboardHeader from "../../DashboardHeader";
 import PullRequestIcon from "assets/pull_request_icon.svg";
 import api from "shared/api";
 import Banner from "components/Banner";
+import Spacer from "components/porter/Spacer";
 
 export const PreviewEnvironmentsHeader = () => {
   const [githubStatus, setGithubStatus] = useState<string>(
@@ -31,12 +32,15 @@ export const PreviewEnvironmentsHeader = () => {
         capitalize={false}
       />
       {githubStatus != "no active incidents" ? (
-        <Banner type="error">
-          GitHub has an ongoing incident.
-          <StyledLink href={`${githubStatus}`} target="_blank">
-            View details
-          </StyledLink>
-        </Banner>
+        <>
+          <Banner type="error">
+            GitHub has an ongoing incident.
+            <StyledLink href={`${githubStatus}`} target="_blank">
+              View details
+            </StyledLink>
+          </Banner>
+          <Spacer y={1} />
+        </>
       ) : null}
     </>
   );

+ 0 - 4
dashboard/src/main/home/modals/EnvEditorModal.tsx

@@ -44,10 +44,6 @@ export default class EnvEditorModal extends Component<PropsType, StateType> {
   render() {
     return (
       <StyledLoadEnvGroupModal>
-        <CloseButton onClick={this.props.closeModal}>
-          <CloseButtonImg src={close} />
-        </CloseButton>
-
         <ModalTitle>Load from Environment Group</ModalTitle>
         <Subtitle>Copy paste your environment file in .env format:</Subtitle>
 

+ 6 - 0
dashboard/src/main/home/provisioner/AWSFormSection.tsx

@@ -62,6 +62,12 @@ const machineTypeOptions = [
   { value: "t3.medium", label: "t3.medium" },
   { value: "t3.xlarge", label: "t3.xlarge" },
   { value: "t3.2xlarge", label: "t3.2xlarge" },
+  { value: "c5.large", label: "c5.large" },
+  { value: "c5.xlarge", label: "c5.xlarge" },
+  { value: "c5.2xlarge", label: "c5.2xlarge" },
+  { value: "m6a.large", label: "m6a.large" },
+  { value: "m6a.xlarge", label: "m6a.xlarge" },
+  { value: "m6a.2xlarge", label: "m6a.2xlarge" },
 ];
 
 const costMapping: Record<string, number> = {

+ 1 - 1
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -106,7 +106,7 @@ export const ClusterSection: React.FC<Props> = ({
               Stacks
             </NavButton>
           ) : null}
-          {currentCluster?.preview_envs_enabled && (
+          {cluster?.preview_envs_enabled && (
             <NavButton
               path="/preview-environments"
               targetClusterName={cluster?.name}

+ 20 - 0
dashboard/src/shared/api.tsx

@@ -2438,6 +2438,25 @@ const removeStackEnvGroup = baseApi<
 
 const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`);
 
+const createSecretAndOpenGitHubPullRequest = baseApi<
+  {
+    github_app_installation_id: number;
+    github_repo_owner: string;
+    github_repo_name: string;
+    open_pr: boolean;
+    branch: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    stack_name: string;
+  }
+>(
+  "POST",
+  ({ project_id, cluster_id, stack_name }) =>
+    `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}/pr`
+);
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
@@ -2643,6 +2662,7 @@ export default {
   createContract,
   getContracts,
   deleteContract,
+  createSecretAndOpenGitHubPullRequest,
   // TRACKING
   updateOnboardingStep,
   // STACKS

+ 13 - 6
dashboard/src/shared/types.tsx

@@ -243,15 +243,15 @@ export interface FormElement {
 export type RepoType = {
   FullName: string;
 } & (
-  | {
+    | {
       Kind: "github";
       GHRepoID: number;
     }
-  | {
+    | {
       Kind: "gitlab";
       GitIntegrationId: number;
     }
-);
+  );
 
 export interface FileType {
   path: string;
@@ -309,15 +309,15 @@ export type ActionConfigType = {
   image_repo_uri: string;
   dockerfile_path?: string;
 } & (
-  | {
+    | {
       kind: "gitlab";
       gitlab_integration_id: number;
     }
-  | {
+    | {
       kind: "github";
       git_repo_id: number;
     }
-);
+  );
 
 export type GithubActionConfigType = ActionConfigType & {
   kind: "github";
@@ -330,6 +330,13 @@ export type FullActionConfigType = ActionConfigType & {
   should_create_workflow: boolean;
 };
 
+export type FullGithubActionConfigType = GithubActionConfigType & {
+  dockerfile_path: string;
+  folder_path: string;
+  registry_id: number;
+  should_create_workflow: boolean;
+};
+
 export interface CapabilityType {
   github: boolean;
   provisioner: boolean;

+ 3 - 3
internal/integrations/ci/actions/actions.go

@@ -80,7 +80,7 @@ func (g *GithubActions) Setup() ([]byte, error) {
 
 	if !g.DryRun {
 		// create porter token secret
-		if err := createGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken, g.GitRepoOwner, g.GitRepoName); err != nil {
+		if err := CreateGithubSecret(client, g.getPorterTokenSecretName(), g.PorterToken, g.GitRepoOwner, g.GitRepoName); err != nil {
 			return nil, err
 		}
 	}
@@ -310,7 +310,7 @@ func (g *GithubActions) getClient() (*github.Client, error) {
 	return github.NewClient(&http.Client{Transport: itr}), nil
 }
 
-func createGithubSecret(
+func CreateGithubSecret(
 	client *github.Client,
 	secretName,
 	secretValue,
@@ -386,7 +386,7 @@ func (g *GithubActions) createEnvSecret(client *github.Client) error {
 
 	secretName := g.getBuildEnvSecretName()
 
-	return createGithubSecret(client, secretName, strings.Join(lines, "\n"), g.GitRepoOwner, g.GitRepoName)
+	return CreateGithubSecret(client, secretName, strings.Join(lines, "\n"), g.GitRepoOwner, g.GitRepoName)
 }
 
 func (g *GithubActions) getWebhookSecretName() string {

+ 1 - 1
internal/integrations/ci/actions/preview.go

@@ -43,7 +43,7 @@ func SetupEnv(opts *EnvOpts) error {
 	}
 
 	// create porter token secret
-	err = createGithubSecret(
+	err = CreateGithubSecret(
 		opts.Client,
 		getPreviewEnvSecretName(opts.ProjectID, opts.ClusterID, opts.InstanceName),
 		opts.PorterToken,

+ 27 - 51
internal/integrations/ci/actions/stack.go

@@ -15,8 +15,9 @@ type GithubPROpts struct {
 	ApplyWorkflowYAML         string
 	StackName                 string
 	ProjectID, ClusterID      uint
-	PorterToken               string
 	ServerURL                 string
+	DefaultBranch             string
+	SecretName                string
 }
 
 type GetStackApplyActionYAMLOpts struct {
@@ -27,56 +28,31 @@ type GetStackApplyActionYAMLOpts struct {
 	SecretName           string
 }
 
-func OpenGithubPR(opts *GithubPROpts) error {
-	// create porter secret
-	secretName := fmt.Sprintf("PORTER_STACK_%d_%d", opts.ProjectID, opts.ClusterID)
-	err := createGithubSecret(
-		opts.Client,
-		secretName,
-		opts.PorterToken,
-		opts.GitRepoOwner,
-		opts.GitRepoName,
-	)
-	if err != nil {
-		return err
-	}
-
-	// get the repository to find the default branch
-	repo, _, err := opts.Client.Repositories.Get(
-		context.TODO(),
-		opts.GitRepoOwner,
-		opts.GitRepoName,
-	)
-	if err != nil {
-		return err
-	}
-
-	defaultBranch := repo.GetDefaultBranch()
-
+func OpenGithubPR(opts *GithubPROpts) (*github.PullRequest, error) {
+	var pr *github.PullRequest
 	applyWorkflowYAML, err := getStackApplyActionYAML(&GetStackApplyActionYAMLOpts{
 		ServerURL:     opts.ServerURL,
 		ClusterID:     opts.ClusterID,
 		ProjectID:     opts.ProjectID,
 		StackName:     opts.StackName,
-		DefaultBranch: defaultBranch,
-		SecretName:    secretName,
+		DefaultBranch: opts.DefaultBranch,
+		SecretName:    opts.SecretName,
 	})
 	if err != nil {
-		return err
+		return pr, err
 	}
 
+	prBranchName := "porter-stack"
+
 	err = createNewBranch(opts.Client,
 		opts.GitRepoOwner,
 		opts.GitRepoName,
-		defaultBranch,
-		"porter-stack")
+		opts.DefaultBranch,
+		prBranchName)
 	if err != nil {
-		return fmt.Errorf(
-			"Unable to create PR to merge workflow files into protected branch: %s.\n"+
-				"To enable Porter Preview Environment deployments, please create Github workflow "+
-				"files in this branch with the following contents:\n"+
-				"--------\n%s--------\nERROR: %w",
-			defaultBranch, string(applyWorkflowYAML), ErrCreatePRForProtectedBranch,
+		return pr, fmt.Errorf(
+			"error creating branch: %w",
+			err,
 		)
 	}
 
@@ -84,30 +60,30 @@ func OpenGithubPR(opts *GithubPROpts) error {
 		opts.Client,
 		fmt.Sprintf("porter_stack_%s.yml", strings.ToLower(opts.StackName)),
 		applyWorkflowYAML, opts.GitRepoOwner,
-		opts.GitRepoName, "porter-preview", false,
+		opts.GitRepoName, prBranchName, false,
 	)
 
 	if err != nil {
-		return fmt.Errorf(
-			"Unable to create PR to merge workflow files into protected branch: %s.\n"+
-				"To enable Porter Preview Environment deployments, please create Github workflow "+
-				"files in this branch with the following contents:\n"+
-				"--------\n%s--------\nERROR: %w",
-			defaultBranch, string(applyWorkflowYAML), ErrCreatePRForProtectedBranch,
+		return pr, fmt.Errorf(
+			"error committing file: %w",
+			err,
 		)
 	}
 
-	_, _, err = opts.Client.PullRequests.Create(
+	pr, _, err = opts.Client.PullRequests.Create(
 		context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.NewPullRequest{
-			Title: github.String("Enable Porter Preview Environment deployments"),
-			Base:  github.String(defaultBranch),
-			Head:  github.String("porter-preview"),
+			Title: github.String("Enable Porter Application"),
+			Base:  github.String(opts.DefaultBranch),
+			Head:  github.String(prBranchName),
 		},
 	)
 	if err != nil {
-		return err
+		return pr, fmt.Errorf(
+			"error creating PR: %w",
+			err,
+		)
 	}
-	return nil
+	return pr, nil
 }
 
 func getStackApplyActionYAML(opts *GetStackApplyActionYAMLOpts) ([]byte, error) {