Skip to content

Commit

Permalink
Porter v2 env (#3582)
Browse files Browse the repository at this point in the history
Co-authored-by: Ian Edwards <[email protected]>
  • Loading branch information
sdess09 and ianedwards authored Sep 15, 2023
1 parent 91e949a commit ab844db
Show file tree
Hide file tree
Showing 10 changed files with 572 additions and 71 deletions.
24 changes: 18 additions & 6 deletions api/server/handlers/porter_app/update_app_environment_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,18 @@ func NewUpdateAppEnvironmentHandler(

// UpdateAppEnvironmentRequest represents the accepted fields on a request to the /apps/{porter_app_name}/environment-group endpoint
type UpdateAppEnvironmentRequest struct {
DeploymentTargetID string `schema:"deployment_target_id"`
Variables map[string]string `schema:"variables"`
Secrets map[string]string `schema:"secrets"`
DeploymentTargetID string `json:"deployment_target_id"`
Variables map[string]string `json:"variables"`
Secrets map[string]string `json:"secrets"`
// HardUpdate is used to remove any variables that are not specified in the request. If false, the request will only update the variables specified in the request,
// and leave all other variables untouched.
HardUpdate bool `schema:"remove_missing"`
HardUpdate bool `json:"remove_missing"`
}

// UpdateAppEnvironmentResponse represents the fields on the response object from the /apps/{porter_app_name}/environment-group endpoint
type UpdateAppEnvironmentResponse struct {
EnvGroupName string `schema:"env_group_name"`
EnvGroupVersion int `schema:"env_group_version"`
EnvGroupName string `json:"env_group_name"`
EnvGroupVersion int `json:"env_group_version"`
}

// ServeHTTP updates or creates the environment group for an app
Expand All @@ -81,6 +81,18 @@ func (c *UpdateAppEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.R
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}
porterApp, err := c.Config().Repo.PorterApp().ReadPorterAppByName(cluster.ID, appName)
if err != nil {
err := telemetry.Error(ctx, span, nil, "error getting porter app by name")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
return
}
if porterApp.ID == 0 {
err := telemetry.Error(ctx, span, nil, "porter app not found")
c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
return
}
telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-id", Value: porterApp.ID})

if request.DeploymentTargetID == "" {
err := telemetry.Error(ctx, span, nil, "must provide deployment target id")
Expand Down
14 changes: 7 additions & 7 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@loadable/component": "^5.15.2",
"@material-ui/core": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61",
"@porter-dev/api-contracts": "^0.0.100",
"@porter-dev/api-contracts": "^0.1.4",
"@react-spring/web": "^9.6.1",
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",
Expand Down Expand Up @@ -147,4 +147,4 @@
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
}
}
12 changes: 8 additions & 4 deletions dashboard/src/lib/hooks/useAppValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,16 @@ export const useAppValidation = ({
if (!deploymentTargetID) {
throw new Error("No deployment target selected");
}

const variables = data.app.env
.filter((e) => !e.deleted)
.reduce((acc: Record<string, string>, curr) => {
acc[curr.key] = curr.value;
return acc;
}, {});
const envVariableDeletions = removedEnvKeys(
data.app.env,
variables,
prevRevision?.env || {}
);

const proto = clientAppToProto(data);
const commit_sha = await match(data.source)
.with({ type: "github" }, async (src) => {
Expand Down Expand Up @@ -125,7 +129,7 @@ export const useAppValidation = ({
atob(validAppData.validate_b64_app_proto)
);

return validatedAppProto;
return { validatedAppProto: validatedAppProto, env: data.app.env };
},
[deploymentTargetID, currentProject, currentCluster]
);
Expand Down
25 changes: 20 additions & 5 deletions dashboard/src/lib/porter-apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,16 @@ export const clientAppValidator = z.object({
readOnly: z.boolean(),
value: z.string(),
}),
envGroups: z.object({ name: z.string(), version: z.bigint() }).array().default([]),
services: serviceValidator.array(),
predeploy: serviceValidator.array().optional(),
env: z.record(z.string(), z.string()).default({}),
env: z.object({
key: z.string(),
value: z.string(),
hidden: z.boolean(),
locked: z.boolean(),
deleted: z.boolean(),
}).array().default([]),
build: buildValidator,
});
export type ClientPorterApp = z.infer<typeof clientAppValidator>;
Expand Down Expand Up @@ -181,7 +188,10 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
new PorterApp({
name: app.name.value,
services,
env: app.env,
envGroups: app.envGroups.map((eg) => ({
name: eg.name,
version: eg.version,
})),
build: clientBuildToProto(app.build),
...(predeploy && {
predeploy: serviceProto(serializeService(predeploy)),
Expand All @@ -194,7 +204,10 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
new PorterApp({
name: app.name.value,
services,
env: app.env,
envGroups: app.envGroups.map((eg) => ({
name: eg.name,
version: eg.version,
})),
image: {
repository: src.image.repository,
tag: src.image.tag,
Expand Down Expand Up @@ -295,7 +308,8 @@ export function clientAppFromProto(
},
services,
predeploy: predeployList,
env: proto.env,
env: [],
envGroups: proto.envGroups.map((eg) => ({ name: eg.name, version: eg.version })),
build: clientBuildFromProto(proto.build) ?? {
method: "pack",
context: "./",
Expand Down Expand Up @@ -326,7 +340,8 @@ export function clientAppFromProto(
},
services,
predeploy,
env: proto.env,
env: [],
envGroups: proto.envGroups.map((eg) => ({ name: eg.name, version: eg.version })),
build: clientBuildFromProto(proto.build) ?? {
method: "pack",
context: "./",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {

const onSubmit = handleSubmit(async (data) => {
try {
const validatedAppProto = await validateApp(data, latestProto);
const { validatedAppProto } = await validateApp(data, latestProto);
await api.applyApp(
"<token>",
{
Expand Down
106 changes: 85 additions & 21 deletions dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import EnvVariables from "../validate-apply/app-settings/EnvVariables";
import { usePorterYaml } from "lib/hooks/usePorterYaml";
import { valueExists } from "shared/util";
import api from "shared/api";
import { PorterApp } from "@porter-dev/api-contracts";
import { EnvGroup, PorterApp } from "@porter-dev/api-contracts";
import GithubActionModal from "../new-app-flow/GithubActionModal";
import { useDefaultDeploymentTarget } from "lib/hooks/useDeploymentTarget";
import Error from "components/porter/Error";
Expand All @@ -44,6 +44,7 @@ import { useAppValidation } from "lib/hooks/useAppValidation";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import PorterYamlModal from "./PorterYamlModal";
import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";

type CreateAppProps = {} & RouteComponentProps;

Expand All @@ -55,7 +56,10 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
count: number;
}>({ detected: false, count: 0 });
const [showGHAModal, setShowGHAModal] = React.useState(false);
const [userHasSeenNoPorterYamlFoundModal, setUserHasSeenNoPorterYamlFoundModal] = React.useState(false);
const [
userHasSeenNoPorterYamlFoundModal,
setUserHasSeenNoPorterYamlFoundModal,
] = React.useState(false);

const [
validatedAppProto,
Expand Down Expand Up @@ -135,7 +139,12 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
const build = watch("app.build");
const image = watch("source.image");
const services = watch("app.services");
const { detectedServices: servicesFromYaml, porterYamlFound, detectedName, loading: isLoadingPorterYaml } = usePorterYaml({ source: source?.type === "github" ? source : null });
const {
detectedServices: servicesFromYaml,
porterYamlFound,
detectedName,
loading: isLoadingPorterYaml,
} = usePorterYaml({ source: source?.type === "github" ? source : null });
const deploymentTarget = useDefaultDeploymentTarget();
const { updateAppStep } = useAppAnalytics(name.value);
const { validateApp } = useAppValidation({
Expand All @@ -146,15 +155,15 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
const onSubmit = handleSubmit(async (data) => {
try {
setDeployError("");
const validatedAppProto = await validateApp(data);
const { validatedAppProto, env } = await validateApp(data);
setValidatedAppProto(validatedAppProto);

if (source.type === "github") {
setShowGHAModal(true);
return;
}

await createAndApply({ app: validatedAppProto, source });
await createAndApply({ app: validatedAppProto, source, env });
} catch (err) {
if (axios.isAxiosError(err) && err.response?.data?.error) {
setDeployError(err.response?.data?.error);
Expand All @@ -170,9 +179,11 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
async ({
app,
source,
env,
}: {
app: PorterApp | null;
source: SourceOptions;
env: KeyValueType[];
}) => {
setIsDeploying(true);
// log analytics event that we started form submission
Expand All @@ -199,10 +210,57 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
}
);

const variables = env
.filter((e) => !e.hidden && !e.deleted)
.reduce((acc: Record<string, string>, item) => {
acc[item.key] = item.value;
return acc;
}, {});
const secrets = env
.filter((e) => !e.deleted)
.reduce((acc: Record<string, string>, item) => {
if (item.hidden) {
acc[item.key] = item.value;
}
return acc;
}, {});
const envGroupResponse = await api.updateEnvironmentGroupV2(
"<token>",
{
deployment_target_id: deploymentTarget.deployment_target_id,
variables: variables,
secrets: secrets,
},
{
id: currentProject.id,
cluster_id: currentCluster.id,
app_name: app.name,
}
);

const addedEnvGroup = await z
.object({
env_group_name: z.string(),
env_group_version: z.number(),
})
.parseAsync(envGroupResponse.data);
const envGroups = [
...app.envGroups,
{
name: addedEnvGroup.env_group_name,
version: addedEnvGroup.env_group_version,
},
];

const appWithSeededEnv = new PorterApp({
...app,
envGroups,
});

await api.applyApp(
"<token>",
{
b64_app_proto: btoa(app.toJsonString()),
b64_app_proto: btoa(appWithSeededEnv.toJsonString()),
deployment_target_id: deploymentTarget.deployment_target_id,
},
{
Expand Down Expand Up @@ -440,21 +498,27 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
source={source}
projectId={currentProject.id}
/>
{!userHasSeenNoPorterYamlFoundModal && !porterYamlFound && !isLoadingPorterYaml &&
<Controller
name="source.porter_yaml_path"
control={control}
render={({ field: { onChange, value } }) => (
<PorterYamlModal
close={() => setUserHasSeenNoPorterYamlFoundModal(true)}
setPorterYamlPath={(porterYamlPath) => {
onChange(porterYamlPath);
}}
porterYamlPath={value}
/>
)}
/>
}
{!userHasSeenNoPorterYamlFoundModal &&
!porterYamlFound &&
!isLoadingPorterYaml && (
<Controller
name="source.porter_yaml_path"
control={control}
render={({ field: { onChange, value } }) => (
<PorterYamlModal
close={() =>
setUserHasSeenNoPorterYamlFoundModal(
true
)
}
setPorterYamlPath={(porterYamlPath) => {
onChange(porterYamlPath);
}}
porterYamlPath={value}
/>
)}
/>
)}
</>
) : (
<ImageSettings />
Expand Down
Loading

0 comments on commit ab844db

Please sign in to comment.