Преглед изворни кода

add helm values tab for porter emails (#3256)

Feroze Mohideen пре 2 година
родитељ
комит
b76a63afaa

+ 38 - 26
api/server/handlers/porter_app/create.go

@@ -107,9 +107,18 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 		// this is required because when the front-end sends an update request with overrideRelease=true, it is unable to
 		// get the image info from the release. unless it is explicitly provided in the request, we avoid overwriting it
-		// by attempting to get the image info from the release
+		// by attempting to get the image info from the release or the provided helm values
 		if helmRelease != nil && (imageInfo.Repository == "" || imageInfo.Tag == "") {
-			imageInfo = attemptToGetImageInfoFromRelease(helmRelease.Config)
+			if request.FullHelmValues != "" {
+				imageInfo, err = attemptToGetImageInfoFromFullHelmValues(request.FullHelmValues)
+				if err != nil {
+					err = telemetry.Error(ctx, span, err, "error getting image info from full helm values")
+					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+					return
+				}
+			} else {
+				imageInfo = attemptToGetImageInfoFromRelease(helmRelease.Config)
+			}
 		}
 	} else {
 		releaseValues = helmRelease.Config
@@ -138,28 +147,31 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		}
 		cloneEnvGroup(c, w, r, k8sAgent, request.EnvGroups, namespace)
 	}
-	chart, values, releaseJobValues, err := parse(
-		porterYaml,
-		imageInfo,
-		c.Config(),
-		cluster.ProjectID,
-		request.UserUpdate,
-		request.EnvGroups,
-		namespace,
-		releaseValues,
-		releaseDependencies,
-		SubdomainCreateOpts{
-			k8sAgent:       k8sAgent,
-			dnsRepo:        c.Repo().DNSRecord(),
-			powerDnsClient: c.Config().PowerDNSClient,
-			appRootDomain:  c.Config().ServerConf.AppRootDomain,
-			stackName:      stackName,
+	chart, values, preDeployJobValues, err := parse(
+		ParseConf{
+			PorterYaml:                porterYaml,
+			ImageInfo:                 imageInfo,
+			ServerConfig:              c.Config(),
+			ProjectID:                 cluster.ProjectID,
+			UserUpdate:                request.UserUpdate,
+			EnvGroups:                 request.EnvGroups,
+			Namespace:                 namespace,
+			ExistingHelmValues:        releaseValues,
+			ExistingChartDependencies: releaseDependencies,
+			SubdomainCreateOpts: SubdomainCreateOpts{
+				k8sAgent:       k8sAgent,
+				dnsRepo:        c.Repo().DNSRecord(),
+				powerDnsClient: c.Config().PowerDNSClient,
+				appRootDomain:  c.Config().ServerConf.AppRootDomain,
+				stackName:      stackName,
+			},
+			InjectLauncherToStartCommand: injectLauncher,
+			ShouldValidateHelmValues:     shouldCreate,
+			FullHelmValues:               request.FullHelmValues,
 		},
-		injectLauncher,
-		shouldCreate,
 	)
 	if err != nil {
-		err = telemetry.Error(ctx, span, err, "error parsing porter yaml into chart and values")
+		err = telemetry.Error(ctx, span, err, "parse error")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
@@ -168,12 +180,12 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-application", Value: true})
 
 		// create the release job chart if it does not exist (only done by front-end currently, where we set overrideRelease=true)
-		if request.OverrideRelease && releaseJobValues != nil {
+		if request.OverrideRelease && preDeployJobValues != nil {
 			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "installing-pre-deploy-job", Value: true})
 			conf, err := createReleaseJobChart(
 				ctx,
 				stackName,
-				releaseJobValues,
+				preDeployJobValues,
 				c.Config().ServerConf.DefaultApplicationHelmRepoURL,
 				registries,
 				cluster,
@@ -272,7 +284,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 		// create/update the release job chart
 		if request.OverrideRelease {
-			if releaseJobValues == nil {
+			if preDeployJobValues == nil {
 				releaseJobName := fmt.Sprintf("%s-r", stackName)
 				_, err := helmAgent.GetRelease(ctx, releaseJobName, 0, false)
 				if err == nil {
@@ -293,7 +305,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 					conf, err := createReleaseJobChart(
 						ctx,
 						stackName,
-						releaseJobValues,
+						preDeployJobValues,
 						c.Config().ServerConf.DefaultApplicationHelmRepoURL,
 						registries,
 						cluster,
@@ -331,7 +343,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 						Cluster:    cluster,
 						Repo:       c.Repo(),
 						Registries: registries,
-						Values:     releaseJobValues,
+						Values:     preDeployJobValues,
 						Chart:      chart,
 					}
 					_, err = helmAgent.UpgradeReleaseByValues(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection, false)

+ 104 - 38
api/server/handlers/porter_app/parse.go

@@ -61,30 +61,56 @@ type SyncedEnvSectionKey struct {
 	Secret bool   `json:"secret" yaml:"secret"`
 }
 
-func parse(
-	porterYaml []byte,
-	imageInfo types.ImageInfo,
-	config *config.Config,
-	projectID uint,
-	userUpdate bool,
-	envGroups []string,
-	namespace string,
-	existingValues map[string]interface{},
-	existingDependencies []*chart.Dependency,
-	opts SubdomainCreateOpts,
-	injectLauncher bool,
-	shouldCreate bool,
-) (*chart.Chart, map[string]interface{}, map[string]interface{}, error) {
+type ParseConf struct {
+	// PorterYaml is the raw porter yaml which is used to build the values + chart for helm upgrade
+	PorterYaml []byte
+	// ImageInfo contains the repository and tag of the image to use for the helm upgrade. Kept separate from the PorterYaml because the image info
+	// is stored in the 'global' key of the values, which is not part of the porter yaml
+	ImageInfo types.ImageInfo
+	// ServerConfig is the server conf, used to find the default helm repo
+	ServerConfig *config.Config
+	// ProjectID
+	ProjectID uint
+	// UserUpdate used for synced env groups
+	UserUpdate bool
+	// EnvGroups used for synced env groups
+	EnvGroups []string
+	// Namespace used for synced env groups
+	Namespace string
+	// ExistingHelmValues is the existing values for the helm release, if it exists
+	ExistingHelmValues map[string]interface{}
+	// ExistingChartDependencies is the existing dependencies for the helm release, if it exists
+	ExistingChartDependencies []*chart.Dependency
+	// SubdomainCreateOpts contains the necessary information to create a subdomain if necessary
+	SubdomainCreateOpts SubdomainCreateOpts
+	// InjectLauncherToStartCommand is a flag to determine whether to prepend the launcher to the start command
+	InjectLauncherToStartCommand bool
+	// ShouldValidateHelmValues is a flag to determine whether to validate helm values
+	ShouldValidateHelmValues bool
+	// FullHelmValues if provided, override anything specified in porter.yaml. Used as an escape hatch for support
+	FullHelmValues string
+}
+
+func parse(conf ParseConf) (*chart.Chart, map[string]interface{}, map[string]interface{}, error) {
 	parsed := &PorterStackYAML{}
 
-	err := yaml.Unmarshal(porterYaml, parsed)
-	if err != nil {
-		return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing porter.yaml", err)
+	if conf.FullHelmValues != "" {
+		parsedHelmValues, err := convertHelmValuesToPorterYaml(conf.FullHelmValues)
+		if err != nil {
+			return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing raw helm values", err)
+		}
+		parsed = parsedHelmValues
+	} else {
+		err := yaml.Unmarshal(conf.PorterYaml, parsed)
+		if err != nil {
+			return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing porter.yaml", err)
+		}
 	}
+
 	synced_env := make([]*SyncedEnvSection, 0)
 
-	for i := range envGroups {
-		cm, _, err := opts.k8sAgent.GetLatestVersionedConfigMap(envGroups[i], namespace)
+	for i := range conf.EnvGroups {
+		cm, _, err := conf.SubdomainCreateOpts.k8sAgent.GetLatestVersionedConfigMap(conf.EnvGroups[i], conf.Namespace)
 		if err != nil {
 			return nil, nil, nil, fmt.Errorf("%s: %w", "error building values from porter.yaml", err)
 		}
@@ -101,7 +127,7 @@ func parse(
 		version := uint(versionInt)
 
 		newSection := &SyncedEnvSection{
-			Name:    envGroups[i],
+			Name:    conf.EnvGroups[i],
 			Version: version,
 		}
 
@@ -119,22 +145,22 @@ func parse(
 	}
 
 	parsed.SyncedEnv = synced_env
-	// 	fmt.Println("This is the config map:" ,cm)
-	values, err := buildUmbrellaChartValues(parsed, imageInfo, existingValues, opts, injectLauncher, shouldCreate, userUpdate)
+
+	values, err := buildUmbrellaChartValues(parsed, conf.ImageInfo, conf.ExistingHelmValues, conf.SubdomainCreateOpts, conf.InjectLauncherToStartCommand, conf.ShouldValidateHelmValues, conf.UserUpdate)
 	if err != nil {
-		return nil, nil, nil, fmt.Errorf("%s: %w", "error building values from porter.yaml", err)
+		return nil, nil, nil, fmt.Errorf("%s: %w", "error building values", err)
 	}
 	convertedValues := convertMap(values).(map[string]interface{})
 
-	chart, err := buildUmbrellaChart(parsed, config, projectID, existingDependencies)
+	chart, err := buildUmbrellaChart(parsed, conf.ServerConfig, conf.ProjectID, conf.ExistingChartDependencies)
 	if err != nil {
-		return nil, nil, nil, fmt.Errorf("%s: %w", "error building chart from porter.yaml", err)
+		return nil, nil, nil, fmt.Errorf("%s: %w", "error building chart", err)
 	}
 
 	// return the parsed release values for the release job chart, if they exist
 	var preDeployJobValues map[string]interface{}
 	if parsed.Release != nil && parsed.Release.Run != nil {
-		preDeployJobValues = buildPreDeployJobChartValues(parsed.Release, parsed.Env, parsed.SyncedEnv, imageInfo, injectLauncher, existingValues, strings.TrimSuffix(strings.TrimPrefix(namespace, "porter-stack-"), "")+"-r", userUpdate)
+		preDeployJobValues = buildPreDeployJobChartValues(parsed.Release, parsed.Env, parsed.SyncedEnv, conf.ImageInfo, conf.InjectLauncherToStartCommand, conf.ExistingHelmValues, strings.TrimSuffix(strings.TrimPrefix(conf.Namespace, "porter-stack-"), "")+"-r", conf.UserUpdate)
 	}
 
 	return chart, convertedValues, preDeployJobValues, nil
@@ -146,14 +172,14 @@ func buildUmbrellaChartValues(
 	existingValues map[string]interface{},
 	opts SubdomainCreateOpts,
 	injectLauncher bool,
-	shouldCreate bool,
+	shouldValidateHelmValues bool,
 	userUpdate bool,
 ) (map[string]interface{}, error) {
 	values := make(map[string]interface{})
 
 	if parsed.Apps == nil {
 		if existingValues == nil {
-			return nil, fmt.Errorf("porter.yaml must contain at least one app, or release must exist and have values")
+			return nil, fmt.Errorf("porter.yaml must contain at least one app, or pre-deploy must exist and have values")
 		}
 	}
 
@@ -173,7 +199,7 @@ func buildUmbrellaChartValues(
 			}
 		}
 
-		validateErr := validateHelmValues(helm_values, shouldCreate, appType)
+		validateErr := validateHelmValues(helm_values, shouldValidateHelmValues, appType)
 		if validateErr != "" {
 			return nil, fmt.Errorf("error validating service \"%s\": %s", name, validateErr)
 		}
@@ -230,9 +256,8 @@ func buildUmbrellaChartValues(
 }
 
 // we can add to this function up later or use an alternative
-func validateHelmValues(values map[string]interface{}, shouldCreate bool, appType string) string {
-	// currently, we only validate port on initial app create, because this will break any updates to existing apps with lower port numbers
-	if shouldCreate {
+func validateHelmValues(values map[string]interface{}, shouldValidateHelmValues bool, appType string) string {
+	if shouldValidateHelmValues {
 		// validate port for web services
 		if appType == "web" {
 			containerMap, err := getNestedMap(values, "container")
@@ -285,12 +310,6 @@ func buildPreDeployJobChartValues(release *App, env map[string]string, synced_en
 	return helm_values
 }
 
-// func populateSyncedEnvGroups(release *App, opts SubdomainCreateOpts) {
-// 	// TODO
-// 	cm, _, err := opts.k8sAgent.GetLatestVersionedConfigMap()
-// 	fmt.Println("This is the config map:" ,cm)
-// }
-
 func getType(name string, app *App) string {
 	if app.Type != nil {
 		return *app.Type
@@ -639,6 +658,17 @@ func getChartTypeFromHelmName(name string) string {
 	return ""
 }
 
+func getServiceNameAndTypeFromHelmName(name string) (string, string) {
+	if strings.HasSuffix(name, "-web") {
+		return strings.TrimSuffix(name, "-web"), "web"
+	} else if strings.HasSuffix(name, "-wkr") {
+		return strings.TrimSuffix(name, "-wkr"), "worker"
+	} else if strings.HasSuffix(name, "-job") {
+		return strings.TrimSuffix(name, "-job"), "job"
+	}
+	return "", ""
+}
+
 func attemptToGetImageInfoFromRelease(values map[string]interface{}) types.ImageInfo {
 	imageInfo := types.ImageInfo{}
 
@@ -661,6 +691,17 @@ func attemptToGetImageInfoFromRelease(values map[string]interface{}) types.Image
 	return imageInfo
 }
 
+func attemptToGetImageInfoFromFullHelmValues(fullHelmValues string) (types.ImageInfo, error) {
+	imageInfo := types.ImageInfo{}
+	var values map[string]interface{}
+	err := yaml.Unmarshal([]byte(fullHelmValues), &values)
+	if err != nil {
+		return imageInfo, fmt.Errorf("error unmarshaling full helm values to read image info: %w", err)
+	}
+	convertedValues := convertMap(values).(map[string]interface{})
+	return attemptToGetImageInfoFromRelease(convertedValues), nil
+}
+
 func getStacksNestedMap(obj map[string]interface{}, fields ...string) ([]map[string]interface{}, error) {
 	var res map[string]interface{}
 	curr := obj
@@ -698,3 +739,28 @@ func getStacksNestedMap(obj map[string]interface{}, fields ...string) ([]map[str
 	}
 	return result, nil
 }
+
+func convertHelmValuesToPorterYaml(helmValues string) (*PorterStackYAML, error) {
+	var values map[string]interface{}
+	err := yaml.Unmarshal([]byte(helmValues), &values)
+	if err != nil {
+		return nil, err
+	}
+	apps := make(map[string]*App)
+	for k, v := range values {
+		if k == "global" {
+			continue
+		}
+		serviceName, serviceType := getServiceNameAndTypeFromHelmName(k)
+		if serviceName == "" {
+			return nil, fmt.Errorf("invalid service key: %s. make sure that service key ends in either -web, -wkr, or -job", k)
+		}
+		apps[serviceName] = &App{
+			Config: convertMap(v).(map[string]interface{}),
+			Type:   &serviceType,
+		}
+	}
+	return &PorterStackYAML{
+		Apps: apps,
+	}, nil
+}

+ 1 - 0
api/types/porter_app.go

@@ -49,6 +49,7 @@ type CreatePorterAppRequest struct {
 	OverrideRelease  bool      `json:"override_release"`
 	EnvGroups        []string  `json:"env_groups"`
 	UserUpdate       bool      `json:"user_update"`
+	FullHelmValues   string    `json:"full_helm_values"`
 }
 
 type UpdatePorterAppRequest struct {

+ 2 - 2
dashboard/src/main/home/app-dashboard/build-settings/BuildSettingsTab.tsx

@@ -4,7 +4,7 @@ import React, {
 } from "react";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
-import { PorterAppOptions } from "shared/types";
+import { CreateUpdatePorterAppOptions } from "shared/types";
 import { Context } from "shared/Context";
 
 import api from "shared/api";
@@ -18,7 +18,7 @@ import _ from "lodash";
 type Props = {
   porterApp: PorterApp;
   setTempPorterApp: (app: PorterApp) => void;
-  updatePorterApp: (options: Partial<PorterAppOptions>) => Promise<void>;
+  updatePorterApp: (options: Partial<CreateUpdatePorterAppOptions>) => Promise<void>;
   clearStatus: () => void;
   buildView: BuildMethod;
   setBuildView: (buildView: BuildMethod) => void;

+ 1 - 1
dashboard/src/main/home/app-dashboard/expanded-app/DiffViewModal.tsx

@@ -16,7 +16,7 @@ import Spacer from "components/porter/Spacer";
 import Checkbox from "components/porter/Checkbox";
 import { NavLink } from "react-router-dom";
 import SidebarLink from "main/home/sidebar/SidebarLink";
-import { EnvVariablesTab } from "./EnvVariablesTab";
+import { EnvVariablesTab } from "./env-vars/EnvVariablesTab";
 type Props = {
   modalVisible: boolean;
   setModalVisible: (x: boolean) => void;

+ 27 - 98
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useContext, useCallback } from "react";
+import React, { useEffect, useState, useContext } from "react";
 import { RouteComponentProps, useParams, withRouter } from "react-router";
 import styled from "styled-components";
 import yaml from "js-yaml";
@@ -13,7 +13,6 @@ import refresh from "assets/refresh.png";
 import save from "assets/save-01.svg";
 
 import api from "shared/api";
-import JSZip from "jszip";
 import { Context } from "shared/Context";
 import Error from "components/porter/Error";
 
@@ -26,7 +25,7 @@ import Link from "components/porter/Link";
 import Back from "components/porter/Back";
 import TabSelector from "components/TabSelector";
 import Icon from "components/porter/Icon";
-import { ChartType, PorterAppOptions } from "shared/types";
+import { ChartType, CreateUpdatePorterAppOptions } from "shared/types";
 import RevisionSection from "main/home/cluster-dashboard/expanded-chart/RevisionSection";
 import BuildSettingsTab from "../build-settings/BuildSettingsTab";
 import Button from "components/porter/Button";
@@ -37,19 +36,18 @@ import Fieldset from "components/porter/Fieldset";
 import { PorterJson, createFinalPorterYaml } from "../new-app-flow/schema";
 import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 import { PorterYamlSchema } from "../new-app-flow/schema";
-import { EnvVariablesTab } from "./EnvVariablesTab";
+import { EnvVariablesTab } from "./env-vars/EnvVariablesTab";
 import GHABanner from "./GHABanner";
 import LogSection from "./logs/LogSection";
 import ActivityFeed from "./activity-feed/ActivityFeed";
 import MetricsSection from "./MetricsSection";
 import StatusSectionFC from "./status/StatusSection";
 import ExpandedJob from "./expanded-job/ExpandedJob";
-import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs";
-import Anser, { AnserJsonEntry } from "anser";
 import _ from "lodash";
 import AnimateHeight from "react-animate-height";
 import { PartialEnvGroup, PopulatedEnvGroup } from "../../../../components/porter-form/types";
 import { BuildMethod, PorterApp } from "../types/porterApp";
+import HelmValuesTab from "./HelmValuesTab";
 
 type Props = RouteComponentProps & {};
 
@@ -70,6 +68,7 @@ const validTabs = [
   "environment",
   "build-settings",
   "settings",
+  "helm-values",
 ] as const;
 const DEFAULT_TAB = "activity";
 type ValidTab = typeof validTabs[number];
@@ -83,7 +82,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     currentCluster,
     currentProject,
     setCurrentError,
-    featurePreview,
+    user,
   } = useContext(Context);
   const [isLoading, setIsLoading] = useState(true);
   const [deleting, setDeleting] = useState(false);
@@ -93,7 +92,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   );
   const [hasBuiltImage, setHasBuiltImage] = useState<boolean>(false);
 
-  const [error, setError] = useState(null);
   const [forceRefreshRevisions, setForceRefreshRevisions] = useState<boolean>(
     false
   );
@@ -110,8 +108,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [showUnsavedChangesBanner, setShowUnsavedChangesBanner] = useState<boolean>(false);
 
   const [expandedJob, setExpandedJob] = useState(null);
-  const [logs, setLogs] = useState<Log[]>([]);
-
   const [services, setServices] = useState<Service[]>([]);
   const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
   const [buttonStatus, setButtonStatus] = useState<React.ReactNode>("");
@@ -126,13 +122,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const { eventId, tab } = useParams<Params>();
   const selectedTab: ValidTab = tab != null && validTabs.includes(tab) ? tab : DEFAULT_TAB;
 
-  useEffect(() => {
-    setBannerLoading(true);
-    getBuildLogs().then(() => {
-      setBannerLoading(false);
-    });
-  }, [appData]);
-
   useEffect(() => {
     if (!_.isEqual(_.omitBy(porterApp, _.isEmpty), _.omitBy(tempPorterApp, _.isEmpty))) {
       setButtonStatus("");
@@ -195,7 +184,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           }
         );
       } catch (err) {
-        setError(err)
+        // that's ok if there's an error, just means there is no pre-deploy chart
       }
 
       // update apps and release
@@ -311,7 +300,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         }
       }
     } catch (err) {
-      setError(err);
+      // TODO: handle error
     } finally {
       setIsLoading(false);
     }
@@ -340,7 +329,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       try {
         await Promise.all(removeApplicationToEnvGroupPromises);
       } catch (error) {
-        setError(error);
+        // TODO: Handle error
       }
     }
     try {
@@ -376,13 +365,13 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       );
       props.history.push("/apps");
     } catch (err) {
-      setError(err);
+      // TODO: handle error
     } finally {
       setDeleting(false);
     }
   };
 
-  const updatePorterApp = async (options: Partial<PorterAppOptions>) => {
+  const updatePorterApp = async (options: Partial<CreateUpdatePorterAppOptions>) => {
     //setting the EnvGroups Config Maps
     const filteredEnvGroups = deletedEnvGroups.filter((deletedEnvGroup) => {
       return !syncedEnvGroups.some((syncedEnvGroup) => {
@@ -434,7 +423,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     try {
       await Promise.all(addApplicationToEnvGroupPromises);
     } catch (error) {
-      setError(error);
+      // TODO: handle error
     }
     try {
       setButtonStatus("loading");
@@ -462,9 +451,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           repo_name: tempPorterApp.repo_name,
           git_branch: tempPorterApp.git_branch,
           buildpacks: "",
-          ...options,
           env_groups: syncedEnvGroups?.map((env) => env.name),
           user_update: true,
+          ...options,
         }
         if (buildView === "docker") {
           updatedPorterApp.dockerfile = tempPorterApp.dockerfile;
@@ -479,7 +468,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         await api.createPorterApp(
           "<token>",
           updatedPorterApp,
-
           {
             cluster_id: currentCluster.id,
             project_id: currentProject.id,
@@ -498,8 +486,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       }
     } catch (err) {
       // TODO: better error handling
-
-      console.log(err);
       const errMessage =
         err?.response?.data?.error ??
         err?.toString() ??
@@ -508,70 +494,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     }
   };
 
-  const getBuildLogs = async () => {
-    try {
-      const res = await api.getGHWorkflowLogs(
-        "",
-        {},
-        {
-          project_id: appData.app.project_id,
-          cluster_id: appData.app.cluster_id,
-          git_installation_id: appData.app.git_repo_id,
-          owner: appData.app.repo_name?.split("/")[0],
-          name: appData.app.repo_name?.split("/")[1],
-          filename: "porter_stack_" + appData.chart.name + ".yml",
-        }
-      );
-      let logs: Log[] = [];
-      if (res.data != null) {
-        // Fetch the logs
-        const logsResponse = await fetch(res.data);
-
-        // Ensure that the response body is only read once
-        const logsBlob = await logsResponse.blob();
-
-        if (logsResponse.headers.get("Content-Type") === "application/zip") {
-          const zip = await JSZip.loadAsync(logsBlob);
-
-          zip.forEach(async function (relativePath, zipEntry) {
-            const fileData = await zip.file(relativePath)?.async("string");
-
-            if (
-              fileData &&
-              fileData.includes("Run porter-dev/porter-cli-action@v0.1.0")
-            ) {
-              const lines = fileData.split("\n");
-              const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z/;
-
-              lines.forEach((line, index) => {
-                const lineWithoutTimestamp = line
-                  .replace(timestampPattern, "")
-                  .trimStart();
-                const anserLine: AnserJsonEntry[] = Anser.ansiToJson(
-                  lineWithoutTimestamp
-                );
-                if (lineWithoutTimestamp.toLowerCase().includes("error")) {
-                  anserLine[0].fg = "238,75,43";
-                }
-
-                const log: Log = {
-                  line: anserLine,
-                  lineNumber: index + 1,
-                  timestamp: line.match(timestampPattern)?.[0],
-                };
-
-                logs.push(log);
-              });
-            }
-          });
-          setLogs(logs);
-        }
-      }
-    } catch (error) {
-      setError(error);
-    }
-  };
-
   const fetchPorterYamlContent = async (
     porterYaml: string,
     appData: any
@@ -698,6 +620,13 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
 
   const renderTabContents = () => {
     switch (selectedTab) {
+      case "activity":
+        return <ActivityFeed
+          chart={appData.chart}
+          stackName={appData?.app?.name}
+          appData={appData}
+          eventId={eventId}
+        />;
       case "overview":
         return (
           <>
@@ -796,13 +725,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             </Button>
           </>
         );
-      case "activity":
-        return <ActivityFeed
-          chart={appData.chart}
-          stackName={appData?.app?.name}
-          appData={appData}
-          eventId={eventId}
-        />;
       case "logs":
         return <LogSection currentChart={appData.chart} services={services} />;
       case "metrics":
@@ -827,6 +749,12 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             setDeletedEnvGroups={setDeleteEnvGroups}
           />
         );
+      case "helm-values":
+        return <HelmValuesTab
+          currentChart={appData.chart}
+          updatePorterApp={updatePorterApp}
+          buttonStatus={buttonStatus}
+        />
       default:
         return <ActivityFeed
           chart={appData.chart}
@@ -1046,6 +974,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                     value: "build-settings",
                   },
                   { label: "Settings", value: "settings" },
+                  user.email.endsWith("porter.run") && { label: "Helm values", value: "helm-values" },
                 ].filter((x) => x)}
                 currentTab={selectedTab}
                 setCurrentTab={(tab: string) => {

+ 84 - 0
dashboard/src/main/home/app-dashboard/expanded-app/HelmValuesTab.tsx

@@ -0,0 +1,84 @@
+import React from "react";
+import styled from "styled-components";
+import yaml from "js-yaml";
+import _ from "lodash";
+
+import { ChartType, CreateUpdatePorterAppOptions } from "shared/types";
+
+import YamlEditor from "components/YamlEditor";
+import Button from "components/porter/Button";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+type Props = {
+    currentChart: ChartType;
+    updatePorterApp: (options: Partial<CreateUpdatePorterAppOptions>) => Promise<void>;
+    buttonStatus: any;
+};
+
+const HelmValuesTab: React.FC<Props> = ({
+    currentChart,
+    updatePorterApp,
+    buttonStatus,
+}) => {
+    const [values, setValues] = React.useState<string>(yaml.dump(currentChart.config));
+
+    const handleSaveValues = async () => {
+        await updatePorterApp({ full_helm_values: values })
+    };
+
+
+    return (
+        <StyledValuesYaml>
+            <Wrapper>
+                <YamlEditor
+                    value={values}
+                    onChange={setValues}
+                    height="calc(100vh - 412px)"
+                />
+            </Wrapper>
+            <Spacer y={0.5} />
+            <Text color="helper">Note: any unsaved service changes from the Overview tab will be lost.</Text>
+            <Spacer y={0.5} />
+            <Button
+                onClick={handleSaveValues}
+                status={buttonStatus}
+                loadingText={"Updating..."}
+            >
+                Update values
+            </Button>
+        </StyledValuesYaml>
+    );
+
+}
+
+export default HelmValuesTab;
+
+const Wrapper = styled.div`
+  overflow: auto;
+  border-radius: 8px;
+  border: 1px solid #ffffff33;
+`;
+
+const StyledValuesYaml = styled.div`
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: calc(100vh - 350px);
+  font-size: 13px;
+  overflow: hidden;
+  border-radius: 8px;
+  animation: floatIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;

+ 2 - 2
dashboard/src/main/home/app-dashboard/expanded-app/activity-feed/events/cards/AppEventCard.tsx

@@ -73,7 +73,7 @@ const AppEventCard: React.FC<Props> = ({ event, appData }) => {
         <Container row>
           <Icon height="16px" src={app_event} />
           <Spacer inline width="10px" />
-          <Text>{event.metadata.detail}</Text>
+          <Text>{event.metadata.summary}</Text>
         </Container>
       </Container>
       <Spacer y={0.5} />
@@ -90,7 +90,7 @@ const AppEventCard: React.FC<Props> = ({ event, appData }) => {
           logs={logs}
           porterAppName={appData.app.name}
           timestamp={readableDate(event.updated_at)}
-          expandedAppEventMessage={event.metadata.summary}
+          expandedAppEventMessage={event.metadata.detail}
         />
       )}
     </StyledEventCard>

+ 5 - 5
dashboard/src/main/home/app-dashboard/expanded-app/EnvVariablesTab.tsx → dashboard/src/main/home/app-dashboard/expanded-app/env-vars/EnvVariablesTab.tsx

@@ -6,12 +6,12 @@ import styled, { keyframes } from "styled-components";
 import Text from "components/porter/Text";
 import Error from "components/porter/Error";
 import sliders from "assets/sliders.svg";
-import EnvGroupModal from "./env-vars/EnvGroupModal";
-import ExpandableEnvGroup from "./env-vars/ExpandableEnvGroup";
-import { PopulatedEnvGroup, PartialEnvGroup } from "../../../../components/porter-form/types";
+import EnvGroupModal from "./EnvGroupModal";
+import ExpandableEnvGroup from "./ExpandableEnvGroup";
+import { PopulatedEnvGroup, PartialEnvGroup } from "../../../../../components/porter-form/types";
 import _, { isObject, differenceBy, omit } from "lodash";
-import api from "../../../../shared/api";
-import { Context } from "../../../../shared/Context";
+import api from "../../../../../shared/api";
+import { Context } from "../../../../../shared/Context";
 
 interface EnvVariablesTabProps {
   envVars: any;

+ 0 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -202,9 +202,6 @@ const WebService = {
                 command: service.startCommand.value,
                 port: service.port.value,
             },
-            service: {
-                port: service.port.value,
-            },
             autoscaling: {
                 enabled: service.autoscaling.enabled.value,
                 minReplicas: service.autoscaling.minReplicas.value,

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

@@ -2,7 +2,7 @@ import { PolicyDocType } from "./auth/types";
 import { PullRequest } from "main/home/cluster-dashboard/preview-environments/types";
 import { baseApi } from "./baseApi";
 
-import { BuildConfig, FullActionConfigType, PorterAppOptions } from "./types";
+import { BuildConfig, FullActionConfigType, CreateUpdatePorterAppOptions } from "./types";
 import {
   CreateStackBody,
   SourceConfig,
@@ -200,7 +200,7 @@ const getPorterAppEvent = baseApi<
 });
 
 const createPorterApp = baseApi<
-  PorterAppOptions,
+  CreateUpdatePorterAppOptions,
   {
     project_id: number;
     cluster_id: number;

+ 2 - 1
dashboard/src/shared/types.tsx

@@ -645,7 +645,7 @@ export type BuildConfig = {
   };
 };
 
-export interface PorterAppOptions {
+export interface CreateUpdatePorterAppOptions {
   porter_yaml: string;
   porter_yaml_path?: string;
   repo_name?: string;
@@ -661,6 +661,7 @@ export interface PorterAppOptions {
     tag: string;
   };
   override_release?: boolean;
+  full_helm_values?: string;
 }
 
 export enum PorterAppEventType {