ソースを参照

Merge branch 'beta.3.procfile-detection-backend' into procfile-support

sunguroku 5 年 前
コミット
8a3d030442

+ 5 - 4
dashboard/src/main/home/Home.tsx

@@ -143,13 +143,13 @@ class Home extends Component<PropsType, StateType> {
     return callback();
   };
 
-  provisionDOKS = async (integrationId: number, region: string) => {
+  provisionDOKS = async (integrationId: number, region: string, clusterName: string) => {
     console.log("Provisioning DOKS...");
     await api.createDOKS(
       "<token>",
       {
         do_integration_id: integrationId,
-        doks_name: this.props.currentProject.name,
+        doks_name: clusterName,
         do_region: region,
       },
       {
@@ -178,17 +178,18 @@ class Home extends Component<PropsType, StateType> {
           let urlParams = new URLSearchParams(queryString);
           let tier = urlParams.get("tier");
           let region = urlParams.get("region");
+          let clusterName = urlParams.get("cluster_name")
           let infras = urlParams.getAll("infras");
           if (infras.length === 2) {
             this.provisionDOCR(tgtIntegration.id, tier, () => {
-              this.provisionDOKS(tgtIntegration.id, region);
+              this.provisionDOKS(tgtIntegration.id, region, clusterName);
             });
           } else if (infras[0] === "docr") {
             this.provisionDOCR(tgtIntegration.id, tier, () => {
               this.props.history.push("dashboard?tab=provisioner");
             });
           } else {
-            this.provisionDOKS(tgtIntegration.id, region);
+            this.provisionDOKS(tgtIntegration.id, region, clusterName);
           }
         })
         .catch(console.log);

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

@@ -28,6 +28,8 @@ type StateType = {
   awsMachineType: string;
   awsAccessId: string;
   awsSecretKey: string;
+  clusterName: string;
+  clusterNameSet: boolean;
   selectedInfras: { value: string; label: string }[];
   buttonStatus: string;
   provisionConfirmed: boolean;
@@ -77,6 +79,8 @@ class AWSFormSection extends Component<PropsType, StateType> {
     awsMachineType: "t2.medium",
     awsAccessId: "",
     awsSecretKey: "",
+    clusterName: "",
+    clusterNameSet: false,
     selectedInfras: [...provisionOptions],
     buttonStatus: "",
     provisionConfirmed: false,
@@ -85,6 +89,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
   componentDidMount = () => {
     let { infras } = this.props;
     let { selectedInfras } = this.state;
+    this.setClusterNameIfNotSet()
 
     if (infras) {
       // From the dashboard, only uncheck and disable if "creating" or "created"
@@ -101,22 +106,38 @@ class AWSFormSection extends Component<PropsType, StateType> {
     }
   };
 
+  componentDidUpdate = (prevProps : PropsType, prevState : StateType) => {
+    if (prevProps.projectName != this.props.projectName) {
+      this.setClusterNameIfNotSet()
+    }
+  }
+
+  setClusterNameIfNotSet = () => {
+    let projectName = this.props.projectName || this.context.currentProject?.name
+
+    if (!this.state.clusterNameSet && !this.state.clusterName.includes(`${projectName}-cluster`)) {
+      this.setState({
+        clusterName: `${projectName}-cluster-${Math.random().toString(36).substring(2, 8)}`
+      })
+    }
+  }
+
   checkFormDisabled = () => {
     if (!this.state.provisionConfirmed) {
       return true;
     }
 
-    let { awsRegion, awsAccessId, awsSecretKey, selectedInfras } = this.state;
+    let { awsRegion, awsAccessId, awsSecretKey, selectedInfras, clusterName } = this.state;
     let { projectName } = this.props;
     if (projectName || projectName === "") {
       return (
         !isAlphanumeric(projectName) ||
-        !(awsAccessId !== "" && awsSecretKey !== "" && awsRegion !== "") ||
+        !(awsAccessId !== "" && awsSecretKey !== "" && awsRegion !== "" && clusterName !== "") ||
         selectedInfras.length === 0
       );
     } else {
       return (
-        !(awsAccessId !== "" && awsSecretKey !== "" && awsRegion !== "") ||
+        !(awsAccessId !== "" && awsSecretKey !== "" && awsRegion !== "" && clusterName !== "") ||
         selectedInfras.length === 0
       );
     }
@@ -188,11 +209,9 @@ class AWSFormSection extends Component<PropsType, StateType> {
   };
 
   provisionEKS = () => {
-    console.log("Provisioning EKS");
-    let { awsAccessId, awsSecretKey, awsRegion, awsMachineType } = this.state;
+    let { awsAccessId, awsSecretKey, awsRegion, awsMachineType, clusterName } = this.state;
     let { currentProject } = this.context;
 
-    let clusterName = `${currentProject.name}-cluster`;
     api
       .createAWSIntegration(
         "<token>",
@@ -266,6 +285,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
       !this.state.awsAccessId ||
       !this.state.awsSecretKey ||
       !this.state.provisionConfirmed ||
+      !this.state.clusterName ||
       this.props.projectName === ""
     ) {
       return "Required fields missing";
@@ -273,6 +293,22 @@ class AWSFormSection extends Component<PropsType, StateType> {
     return this.state.buttonStatus;
   };
 
+  renderClusterNameSection = () => {
+    let { selectedInfras, clusterName } = this.state;
+
+    if (selectedInfras.length == 2 ||  (selectedInfras.length == 1 && selectedInfras[0].value === "eks")) {
+      return <InputRow
+        type="text"
+        value={clusterName}
+        setValue={(x: string) => this.setState({ clusterName: x, clusterNameSet: true })}
+        label="Cluster Name"
+        placeholder="ex: porter-cluster"
+        width="100%"
+        isRequired={true}
+      />
+    }
+  }
+
   render() {
     let { setSelectedProvisioner } = this.props;
     let {
@@ -345,6 +381,7 @@ class AWSFormSection extends Component<PropsType, StateType> {
               this.setState({ selectedInfras: x });
             }}
           />
+          {this.renderClusterNameSection()}
           <Helper>
             By default, Porter creates a cluster with three t2.medium instances
             (2vCPUs and 4GB RAM each). AWS will bill you for any provisioned

+ 45 - 6
dashboard/src/main/home/provisioner/DOFormSection.tsx

@@ -7,6 +7,7 @@ import api from "shared/api";
 import { Context } from "shared/Context";
 import { InfraType } from "shared/types";
 
+import InputRow from "components/values-form/InputRow";
 import CheckboxRow from "components/values-form/CheckboxRow";
 import SelectRow from "components/values-form/SelectRow";
 import Helper from "components/values-form/Helper";
@@ -25,6 +26,8 @@ type StateType = {
   selectedInfras: { value: string; label: string }[];
   subscriptionTier: string;
   doRegion: string;
+  clusterName: string;
+  clusterNameSet: boolean;
   provisionConfirmed: boolean;
 };
 
@@ -57,12 +60,15 @@ export default class DOFormSection extends Component<PropsType, StateType> {
     selectedInfras: [...provisionOptions],
     subscriptionTier: "basic",
     doRegion: "nyc1",
+    clusterName: "",
+    clusterNameSet: false,
     provisionConfirmed: false,
   };
 
   componentDidMount = () => {
     let { infras } = this.props;
     let { selectedInfras } = this.state;
+    this.setClusterNameIfNotSet()
 
     if (infras) {
       // From the dashboard, only uncheck and disable if "creating" or "created"
@@ -79,17 +85,33 @@ export default class DOFormSection extends Component<PropsType, StateType> {
     }
   };
 
+  componentDidUpdate = (prevProps : PropsType, prevState : StateType) => {
+    if (prevProps.projectName != this.props.projectName) {
+      this.setClusterNameIfNotSet()
+    }
+  }
+
+  setClusterNameIfNotSet = () => {
+    let projectName = this.props.projectName || this.context.currentProject?.name
+
+    if (!this.state.clusterNameSet && !this.state.clusterName.includes(`${projectName}-cluster`)) {
+      this.setState({
+        clusterName: `${projectName}-cluster-${Math.random().toString(36).substring(2, 8)}`
+      })
+    }
+  }
+
   checkFormDisabled = () => {
     if (!this.state.provisionConfirmed) {
       return true;
     }
 
-    let { selectedInfras } = this.state;
+    let { selectedInfras, clusterName } = this.state;
     let { projectName } = this.props;
     if (projectName || projectName === "") {
-      return !isAlphanumeric(projectName) || selectedInfras.length === 0;
+      return !isAlphanumeric(projectName) || selectedInfras.length === 0 || !clusterName;
     } else {
-      return selectedInfras.length === 0;
+      return selectedInfras.length === 0 || !clusterName;
     }
   };
 
@@ -127,9 +149,9 @@ export default class DOFormSection extends Component<PropsType, StateType> {
   };
 
   doRedirect = (projectId: number) => {
-    let { subscriptionTier, doRegion, selectedInfras } = this.state;
+    let { subscriptionTier, doRegion, selectedInfras, clusterName } = this.state;
     let redirectUrl = `/api/oauth/projects/${projectId}/digitalocean?project_id=${projectId}&provision=do`;
-    redirectUrl += `&tier=${subscriptionTier}&region=${doRegion}`;
+    redirectUrl += `&tier=${subscriptionTier}&region=${doRegion}&cluster_name=${clusterName}`;
     selectedInfras.forEach((option: { value: string; label: string }) => {
       redirectUrl += `&infras=${option.value}`;
     });
@@ -156,11 +178,27 @@ export default class DOFormSection extends Component<PropsType, StateType> {
         return "Project name contains illegal characters";
       }
     }
-    if (!this.state.provisionConfirmed || this.props.projectName === "") {
+    if (!this.state.provisionConfirmed || this.props.projectName === "" || !this.state.clusterName) {
       return "Required fields missing";
     }
   };
 
+  renderClusterNameSection = () => {
+    let { selectedInfras, clusterName } = this.state;
+
+    if (selectedInfras.length == 2 ||  (selectedInfras.length == 1 && selectedInfras[0].value === "doks")) {
+      return <InputRow
+        type="text"
+        value={clusterName}
+        setValue={(x: string) => this.setState({ clusterName: x, clusterNameSet: true })}
+        label="Cluster Name"
+        placeholder="ex: porter-cluster"
+        width="100%"
+        isRequired={true}
+      />
+    }
+  }
+
   render() {
     let { setSelectedProvisioner } = this.props;
     let { selectedInfras, subscriptionTier, doRegion } = this.state;
@@ -202,6 +240,7 @@ export default class DOFormSection extends Component<PropsType, StateType> {
               this.setState({ selectedInfras: x });
             }}
           />
+          {this.renderClusterNameSection()}
           <Helper>
             By default, Porter creates a cluster with three Standard (2vCPUs /
             2GB RAM) droplets. DigitalOcean will bill you for any provisioned

+ 43 - 5
dashboard/src/main/home/provisioner/GCPFormSection.tsx

@@ -27,6 +27,8 @@ type StateType = {
   gcpRegion: string;
   gcpProjectId: string;
   gcpKeyData: string;
+  clusterName: string;
+  clusterNameSet: boolean;
   selectedInfras: { value: string; label: string }[];
   buttonStatus: string;
   provisionConfirmed: boolean;
@@ -69,6 +71,8 @@ class GCPFormSection extends Component<PropsType, StateType> {
     gcpRegion: "us-east1",
     gcpProjectId: "",
     gcpKeyData: "",
+    clusterName: "",
+    clusterNameSet: false,
     selectedInfras: [...provisionOptions],
     buttonStatus: "",
     provisionConfirmed: false,
@@ -77,6 +81,7 @@ class GCPFormSection extends Component<PropsType, StateType> {
   componentDidMount = () => {
     let { infras } = this.props;
     let { selectedInfras } = this.state;
+    this.setClusterNameIfNotSet()
 
     if (infras) {
       // From the dashboard, only uncheck and disable if "creating" or "created"
@@ -93,22 +98,38 @@ class GCPFormSection extends Component<PropsType, StateType> {
     }
   };
 
+  componentDidUpdate = (prevProps : PropsType, prevState : StateType) => {
+    if (prevProps.projectName != this.props.projectName) {
+      this.setClusterNameIfNotSet()
+    }
+  }
+
+  setClusterNameIfNotSet = () => {
+    let projectName = this.props.projectName || this.context.currentProject?.name
+
+    if (!this.state.clusterNameSet && !this.state.clusterName.includes(`${projectName}-cluster`)) {
+      this.setState({
+        clusterName: `${projectName}-cluster-${Math.random().toString(36).substring(2, 8)}`
+      })
+    }
+  }
+
   checkFormDisabled = () => {
     if (!this.state.provisionConfirmed) {
       return true;
     }
 
-    let { gcpRegion, gcpProjectId, gcpKeyData, selectedInfras } = this.state;
+    let { gcpRegion, gcpProjectId, gcpKeyData, selectedInfras, clusterName } = this.state;
     let { projectName } = this.props;
     if (projectName || projectName === "") {
       return (
         !isAlphanumeric(projectName) ||
-        !(gcpProjectId !== "" && gcpKeyData !== "" && gcpRegion !== "") ||
+        !(gcpProjectId !== "" && gcpKeyData !== "" && gcpRegion !== "" && clusterName !== "") ||
         selectedInfras.length === 0
       );
     } else {
       return (
-        !(gcpProjectId !== "" && gcpKeyData !== "" && gcpRegion !== "") ||
+        !(gcpProjectId !== "" && gcpKeyData !== "" && gcpRegion !== "" && clusterName !== "") ||
         selectedInfras.length === 0
       );
     }
@@ -170,12 +191,11 @@ class GCPFormSection extends Component<PropsType, StateType> {
     let { handleError } = this.props;
     let { currentProject } = this.context;
 
-    let clusterName = `${currentProject.name}-cluster`;
     api
       .createGKE(
         "<token>",
         {
-          gke_name: clusterName,
+          gke_name: this.state.clusterName,
           gcp_integration_id: id,
         },
         { project_id: currentProject.id }
@@ -243,6 +263,7 @@ class GCPFormSection extends Component<PropsType, StateType> {
       !this.state.gcpProjectId ||
       !this.state.gcpKeyData ||
       !this.state.provisionConfirmed ||
+      !this.state.clusterName ||
       this.props.projectName === ""
     ) {
       return "Required fields missing";
@@ -250,6 +271,22 @@ class GCPFormSection extends Component<PropsType, StateType> {
     return this.state.buttonStatus;
   };
 
+  renderClusterNameSection = () => {
+    let { selectedInfras, clusterName } = this.state;
+
+    if (selectedInfras.length == 2 ||  (selectedInfras.length == 1 && selectedInfras[0].value === "gke")) {
+      return <InputRow
+        type="text"
+        value={clusterName}
+        setValue={(x: string) => this.setState({ clusterName: x, clusterNameSet: true })}
+        label="Cluster Name"
+        placeholder="ex: porter-cluster"
+        width="100%"
+        isRequired={true}
+      />
+    }
+  }
+
   render() {
     let { setSelectedProvisioner } = this.props;
     let { gcpRegion, gcpProjectId, gcpKeyData, selectedInfras } = this.state;
@@ -308,6 +345,7 @@ class GCPFormSection extends Component<PropsType, StateType> {
               this.setState({ selectedInfras: x });
             }}
           />
+          {this.renderClusterNameSection()}
           <Helper>
             By default, Porter creates a cluster with three e2-medium instances
             (2vCPUs and 4GB RAM each). Google Cloud will bill you for any

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

@@ -297,6 +297,22 @@ const getBranchContents = baseApi<
   return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id}/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name}/${pathParams.branch}/contents`;
 });
 
+const getProcfileContents = baseApi<
+  {
+    path: string;
+  },
+  {
+    project_id: number;
+    git_repo_id: number;
+    kind: string;
+    owner: string;
+    name: string;
+    branch: string;
+  }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id}/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name}/${pathParams.branch}/procfile`;
+});
+
 const getBranches = baseApi<
   {},
   {
@@ -821,6 +837,7 @@ export default {
   getNamespaces,
   getNGINXIngresses,
   getOAuthIds,
+  getProcfileContents,
   getProjectClusters,
   getProjectRegistries,
   getProjectRepos,

+ 2 - 2
internal/integrations/ci/actions/steps.go

@@ -46,7 +46,7 @@ func getConfigurePorterStep(porterTokenSecretName string) GithubActionYAMLStep {
 const dockerBuildPush string = `
 export $(echo "${{secrets.%s}}" | xargs)
 echo "${{secrets.%s}}" > ./env_porter
-sudo docker build %s $(cat ./env_porter | sed 's@^@--build-arg @g' | paste -s -d " " -) --file %s -t %s:$(git rev-parse --short HEAD)
+sudo docker build %s $(cat ./env_porter | awk 'NF' | sed 's@^@--build-arg @g' | paste -s -d " " -) --file %s -t %s:$(git rev-parse --short HEAD)
 sudo docker push %s:$(git rev-parse --short HEAD)
 `
 
@@ -54,7 +54,7 @@ func getDockerBuildPushStep(envSecretName, dockerFilePath, repoURL string) Githu
 	return GithubActionYAMLStep{
 		Name: "Docker build, push",
 		ID:   "docker_build_push",
-		Run:  fmt.Sprintf(dockerBuildPush, envSecretName, filepath.Dir(dockerFilePath), dockerFilePath, repoURL, repoURL),
+		Run:  fmt.Sprintf(dockerBuildPush, envSecretName, envSecretName, filepath.Dir(dockerFilePath), dockerFilePath, repoURL, repoURL),
 	}
 }
 

+ 0 - 2
server/api/git_action_handler.go

@@ -151,8 +151,6 @@ func (app *App) createGitActionFromForm(
 		return nil
 	}
 
-	fmt.Println("GIT ACTIONB BRANCH IS", gitAction.GitBranch)
-
 	// create the commit in the git repo
 	gaRunner := &actions.GithubActions{
 		GitIntegration: gr,

+ 61 - 0
server/api/git_repo_handler.go

@@ -6,7 +6,9 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"regexp"
 	"strconv"
+	"strings"
 
 	"golang.org/x/oauth2"
 
@@ -202,6 +204,65 @@ func (app *App) HandleGetBranchContents(w http.ResponseWriter, r *http.Request)
 	json.NewEncoder(w).Encode(res)
 }
 
+type GetProcfileContentsResp map[string]string
+
+var procfileRegex = regexp.MustCompile("^([A-Za-z0-9_]+):\\s*(.+)$")
+
+// HandleGetProcfileContents retrieves the contents of a procfile in a github repo
+func (app *App) HandleGetProcfileContents(w http.ResponseWriter, r *http.Request) {
+	tok, err := app.githubTokenFromRequest(r)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	client := github.NewClient(app.GithubProjectConf.Client(oauth2.NoContext, tok))
+	owner := chi.URLParam(r, "owner")
+	name := chi.URLParam(r, "name")
+	branch := chi.URLParam(r, "branch")
+
+	queryParams, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	resp, _, _, err := client.Repositories.GetContents(
+		context.TODO(),
+		owner,
+		name,
+		queryParams["path"][0],
+		&github.RepositoryContentGetOptions{
+			Ref: branch,
+		},
+	)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	fileData, err := resp.GetContent()
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	parsedContents := make(GetProcfileContentsResp)
+
+	// parse the procfile information
+	for _, line := range strings.Split(fileData, "\n") {
+		if matches := procfileRegex.FindStringSubmatch(line); matches != nil {
+			parsedContents[matches[1]] = matches[2]
+		}
+	}
+
+	json.NewEncoder(w).Encode(parsedContents)
+}
+
 // finds the github token given the git repo id and the project id
 func (app *App) githubTokenFromRequest(
 	r *http.Request,

+ 14 - 0
server/router/router.go

@@ -1053,6 +1053,20 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/gitrepos/{git_repo_id}/repos/{kind}/{owner}/{name}/{branch}/procfile",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveGitRepoAccess(
+					requestlog.NewHandler(a.HandleGetProcfileContents, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		// /api/projects/{project_id}/deploy routes
 		r.Method(
 			"POST",