Forráskód Böngészése

Stacks inject port env variable and launcher to start command if heroku buildpacks (#3030)

Feroze Mohideen 3 éve
szülő
commit
9179b41a2c

+ 2 - 0
.gitignore

@@ -74,3 +74,5 @@ terraform.rc
 .vscode
 
 tmp
+
+tsconfig.json

+ 7 - 1
api/server/handlers/stacks/create_porter_app.go

@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -98,6 +99,9 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		releaseDependencies = helmRelease.Chart.Metadata.Dependencies
 	}
 
+	injectLauncher := strings.Contains(request.Builder, "heroku") ||
+		strings.Contains(request.Builder, "paketo")
+
 	chart, values, releaseJobValues, err := parse(
 		porterYaml,
 		imageInfo,
@@ -111,7 +115,9 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			powerDnsClient: c.Config().PowerDNSClient,
 			appRootDomain:  c.Config().ServerConf.AppRootDomain,
 			stackName:      stackName,
-		})
+		},
+		injectLauncher,
+	)
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing porter yaml into chart and values: %w", err)))
 		return

+ 31 - 21
api/server/handlers/stacks/parse.go

@@ -56,6 +56,7 @@ func parse(
 	existingValues map[string]interface{},
 	existingDependencies []*chart.Dependency,
 	opts SubdomainCreateOpts,
+	injectLauncher bool,
 ) (*chart.Chart, map[string]interface{}, map[string]interface{}, error) {
 	parsed := &PorterStackYAML{}
 
@@ -64,7 +65,7 @@ func parse(
 		return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing porter.yaml", err)
 	}
 
-	values, err := buildStackValues(parsed, imageInfo, existingValues, opts)
+	values, err := buildStackValues(parsed, imageInfo, existingValues, opts, injectLauncher)
 	if err != nil {
 		return nil, nil, nil, fmt.Errorf("%s: %w", "error building values from porter.yaml", err)
 	}
@@ -78,13 +79,13 @@ func parse(
 	// return the parsed release values for the release job chart, if they exist
 	var releaseJobValues map[string]interface{}
 	if parsed.Release != nil && parsed.Release.Run != nil {
-		releaseJobValues = buildReleaseValues(parsed.Release, parsed.Env, imageInfo)
+		releaseJobValues = buildReleaseValues(parsed.Release, parsed.Env, imageInfo, injectLauncher)
 	}
 
 	return chart, convertedValues, releaseJobValues, nil
 }
 
-func buildStackValues(parsed *PorterStackYAML, imageInfo types.ImageInfo, existingValues map[string]interface{}, opts SubdomainCreateOpts) (map[string]interface{}, error) {
+func buildStackValues(parsed *PorterStackYAML, imageInfo types.ImageInfo, existingValues map[string]interface{}, opts SubdomainCreateOpts, injectLauncher bool) (map[string]interface{}, error) {
 	values := make(map[string]interface{})
 
 	if parsed.Apps == nil {
@@ -122,6 +123,17 @@ func buildStackValues(parsed *PorterStackYAML, imageInfo types.ImageInfo, existi
 			}
 		}
 
+		// prepend launcher if we need to
+		if helm_values["container"] != nil {
+			containerMap := helm_values["container"].(map[string]interface{})
+			if containerMap["command"] != nil {
+				command := containerMap["command"].(string)
+				if injectLauncher && !strings.HasPrefix(command, "launcher") && !strings.HasPrefix(command, "/cnb/lifecycle/launcher") {
+					containerMap["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", command)
+				}
+			}
+		}
+
 		values[helmName] = helm_values
 	}
 
@@ -144,7 +156,7 @@ func buildStackValues(parsed *PorterStackYAML, imageInfo types.ImageInfo, existi
 	return values, nil
 }
 
-func buildReleaseValues(release *App, env map[string]string, imageInfo types.ImageInfo) map[string]interface{} {
+func buildReleaseValues(release *App, env map[string]string, imageInfo types.ImageInfo, injectLauncher bool) map[string]interface{} {
 	defaultValues := getDefaultValues(release, env, "job")
 	convertedConfig := convertMap(release.Config).(map[string]interface{})
 	helm_values := utils.DeepCoalesceValues(defaultValues, convertedConfig)
@@ -156,6 +168,14 @@ func buildReleaseValues(release *App, env map[string]string, imageInfo types.Ima
 		}
 	}
 
+	// prepend launcher if we need to
+	if injectLauncher && release.Run != nil && !strings.HasPrefix(*release.Run, "launcher") && !strings.HasPrefix(*release.Run, "/cnb/lifecycle/launcher") {
+		if helm_values["container"] == nil {
+			helm_values["container"] = map[string]interface{}{}
+		}
+		helm_values["container"].(map[string]interface{})["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", *release.Run)
+	}
+
 	return helm_values
 }
 
@@ -175,25 +195,15 @@ func getDefaultValues(app *App, env map[string]string, appType string) map[strin
 	if app.Run != nil {
 		runCommand = *app.Run
 	}
-	if appType == "web" {
-		defaultValues = map[string]interface{}{
-			"container": map[string]interface{}{
-				"command": runCommand,
-				"env": map[string]interface{}{
-					"normal": CopyEnv(env),
-				},
-			},
-		}
-	} else {
-		defaultValues = map[string]interface{}{
-			"container": map[string]interface{}{
-				"command": runCommand,
-				"env": map[string]interface{}{
-					"normal": CopyEnv(env),
-				},
+	defaultValues = map[string]interface{}{
+		"container": map[string]interface{}{
+			"command": runCommand,
+			"env": map[string]interface{}{
+				"normal": CopyEnv(env),
 			},
-		}
+		},
 	}
+
 	return defaultValues
 }
 

+ 7 - 4
dashboard/src/main/home/app-dashboard/expanded-app/BuildSettingsTabStack.tsx

@@ -99,7 +99,7 @@ const BuildSettingsTabStack: React.FC<Props> = ({
 
   const triggerWorkflow = async () => {
     try {
-      await api.reRunGHWorkflow(
+      const res = await api.reRunGHWorkflow(
         "",
         {},
         {
@@ -112,6 +112,9 @@ const BuildSettingsTabStack: React.FC<Props> = ({
           filename: "porter_stack_" + appData.chart.name + ".yml",
         }
       );
+      if (res.data != null) {
+        window.open(res.data, "_blank", "noreferrer")
+      }
     } catch (error) {
       if (!error?.response) {
         throw error;
@@ -149,7 +152,7 @@ const BuildSettingsTabStack: React.FC<Props> = ({
         }
         setCurrentError(
           'The workflow is still running. You can "Save" the current build settings for the next workflow run and view the current status of the workflow here: ' +
-            tmpError.response.data
+          tmpError.response.data
         );
         return;
       }
@@ -232,7 +235,7 @@ const BuildSettingsTabStack: React.FC<Props> = ({
         label="GitHub repository:"
         width="100%"
         value={actionConfig?.git_repo}
-        setValue={() => {}}
+        setValue={() => { }}
         placeholder=""
       />
       <Spacer y={0.5} />
@@ -348,7 +351,7 @@ const StyledAdvancedBuildSettings = styled.div`
     cursor: pointer;
     border-radius: 20px;
     transform: ${(props: { showSettings: boolean; isCurrent: boolean }) =>
-      props.showSettings ? "" : "rotate(-90deg)"};
+    props.showSettings ? "" : "rotate(-90deg)"};
   }
 `;
 const StyledSourceBox = styled.div`

+ 4 - 2
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -252,7 +252,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           services,
           releaseJob,
           envVars,
-          porterJson
+          porterJson,
+          // if we are using a heroku buildpack, inject a PORT env variable
+          appData.app.builder != null && appData.app.builder.includes("heroku")
         );
         const yamlString = yaml.dump(finalPorterYaml);
         const base64Encoded = btoa(yamlString);
@@ -626,7 +628,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                 <Fieldset>
                   <Container row>
                     <PlaceholderIcon src={notFound} />
-                    <Text color="helper">No pre-deploy jobs were found.</Text>
+                    <Text color="helper">No pre-deploy jobs were found. Add a pre-deploy job to perform an operation before your application services deploy, like a database migration.</Text>
                   </Container>
                 </Fieldset>
                 <Spacer y={0.5} />

+ 9 - 7
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -156,6 +156,9 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
 
   const updateStackStep = async (step: string) => {
     try {
+      if (currentCluster?.id == null || currentProject?.id == null) {
+        throw "Unable to capture analytics, project or cluster not found";
+      }
       await api.updateStackStep(
         "<token>",
         {
@@ -209,8 +212,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       ) {
         setDetected({
           detected: true,
-          message: `Detected ${Object.keys(porterYamlToJson.apps).length
-            } services from porter.yaml`,
+          message: `Detected ${Object.keys(porterYamlToJson.apps).length} service${Object.keys(porterYamlToJson.apps).length === 1 ? "" : "s"} from porter.yaml`,
         });
       } else {
         setDetected({
@@ -311,10 +313,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       await updateStackStep('stack-launch-complete');
 
       if (
-        currentProject == null ||
-        currentCluster == null ||
-        currentProject.id == null ||
-        currentCluster.id == null
+        currentProject?.id == null ||
+        currentCluster?.id == null
       ) {
         throw "Project or cluster not found";
       }
@@ -324,7 +324,9 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         formState.serviceList,
         formState.releaseJob,
         formState.envVariables,
-        porterJson
+        porterJson,
+        // if we are using a heroku buildpack, inject a PORT env variable
+        (buildConfig as any)?.builder != null && (buildConfig as any)?.builder.includes("heroku")
       );
 
       const yamlString = yaml.dump(finalPorterYaml);

+ 23 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx

@@ -26,6 +26,19 @@ const WebTabs: React.FC<Props> = ({
         <Spacer y={1} />
         <Input
           label="Start command"
+          // TODO: uncomment the below once we have docs on what /cnb/lifecycle/launcher is
+          // label={
+          //   <>
+          //     <span>Start command</span>
+          //     {!service.startCommand.readOnly && service.startCommand.value.includes("/cnb/lifecycle/launcher") &&
+          //       <a
+          //         href="https://docs.porter.run/deploying-applications/https-and-domains/custom-domains"
+          //         target="_blank"
+          //       >
+          //         &nbsp;(?)
+          //       </a>
+          //     }
+          //   </>}
           placeholder="ex: sh start.sh"
           value={service.startCommand.value}
           width="300px"
@@ -147,7 +160,16 @@ const WebTabs: React.FC<Props> = ({
       <>
         <Spacer y={1} />
         <Input
-          label="Custom domain"
+          label={
+            <>
+              <span>Custom domain</span>
+              <a
+                href="https://docs.porter.run/deploying-applications/https-and-domains/custom-domains"
+                target="_blank"
+              >
+                &nbsp;(?)
+              </a>
+            </>}
           placeholder="ex: my-app.my-domain.com"
           value={service.ingress.hosts.value}
           disabled={service.ingress.hosts.readOnly}

+ 19 - 4
dashboard/src/main/home/app-dashboard/new-app-flow/schema.tsx

@@ -48,11 +48,20 @@ export const createFinalPorterYaml = (
     releaseJob: Service[],
     dashboardSetEnvVariables: KeyValueType[],
     porterJson: PorterJson | undefined,
+    injectPortEnvVariable: boolean = false,
 ): PorterJson => {
+    const [apps, port] = createApps(services.filter(Service.isNonRelease), porterJson, injectPortEnvVariable);
+    const env = combineEnv(dashboardSetEnvVariables, porterJson?.env);
+
+    // inject a port env variable if necessary
+    if (port != null) {
+        env.PORT = port;
+    }
+
     return {
         version: "v1stack",
-        env: combineEnv(dashboardSetEnvVariables, porterJson?.env),
-        apps: createApps(services.filter(Service.isNonRelease), porterJson),
+        env,
+        apps,
         release: createRelease(releaseJob.find(Service.isRelease)),
     };
 };
@@ -76,8 +85,10 @@ const combineEnv = (
 const createApps = (
     serviceList: (WorkerService | WebService | JobService)[],
     porterJson: PorterJson | undefined,
-): z.infer<typeof AppsSchema> => {
+    injectPortEnvVariable: boolean,
+): [z.infer<typeof AppsSchema>, string | undefined] => {
     const apps: z.infer<typeof AppsSchema> = {};
+    let port: string | undefined = undefined;
     for (const service of serviceList) {
         let config = Service.serialize(service);
 
@@ -93,6 +104,10 @@ const createApps = (
             );
         }
 
+        if (injectPortEnvVariable && service.type === "web") {
+            port = service.port.value;
+        }
+
         apps[service.name] = {
             type: service.type,
             run: service.startCommand.value,
@@ -100,7 +115,7 @@ const createApps = (
         };
     }
 
-    return apps;
+    return [apps, port];
 };
 
 const createRelease = (

+ 1 - 2
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -338,7 +338,6 @@ export const Service = {
         if (defaultValues == null) {
             return [];
         }
-
         return Object.keys(defaultValues).map((name: string) => {
             const suffix = name.slice(-4);
             if (suffix in SUFFIX_TO_TYPE) {
@@ -357,7 +356,7 @@ export const Service = {
                         return JobService.deserialize(appName, coalescedValues, porterJson);
                 }
             }
-        }).filter((service: Service | undefined): service is Service => service != null);
+        }).filter((service: Service | undefined): service is Service => service != null) as Service[];
     },
     // TODO: consolidate these
     deserializeRelease: (helmValues: any, porterJson?: PorterJson): ReleaseService => {