From a0f69aecfdc4c1f6a733eb74699350d7a5d7ee10 Mon Sep 17 00:00:00 2001 From: ianedwards Date: Mon, 9 Oct 2023 10:43:12 -0400 Subject: [PATCH] custom nginx ingress annotations on web svcs (#3765) --- dashboard/package-lock.json | 26 ++--- dashboard/package.json | 2 +- dashboard/src/lib/porter-apps/services.ts | 34 ++++++ dashboard/src/lib/porter-apps/values.ts | 90 +++++++++------ .../tabs/IngressCustomAnnotations.tsx | 107 ++++++++++++++++++ .../services-settings/tabs/Networking.tsx | 2 + go.mod | 2 +- go.sum | 4 +- 8 files changed, 216 insertions(+), 51 deletions(-) create mode 100644 dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IngressCustomAnnotations.tsx diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index bca1197242..48e1c923c1 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -13,7 +13,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.2.8", + "@porter-dev/api-contracts": "^0.2.11", "@react-spring/web": "^9.6.1", "@sentry/react": "^6.13.2", "@sentry/tracing": "^6.13.2", @@ -1953,9 +1953,9 @@ } }, "node_modules/@bufbuild/protobuf": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.1.tgz", - "integrity": "sha512-BUyJWutgP2S8K/1NphOJokuwDckXS4qI2T1pGZAlkFdZchWae3jm6fCdkcGbLlM1QLOcNFFePd+7Feo4BYGrJQ==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.3.tgz", + "integrity": "sha512-AoHSiIpTFF97SQgmQni4c+Tyr0CDhkaRaR2qGEJTEbauqQwLRpLrd9yVv//wVHOSxr/b4FJcL54VchhY6710xA==" }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", @@ -2455,9 +2455,9 @@ } }, "node_modules/@porter-dev/api-contracts": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.8.tgz", - "integrity": "sha512-2eNrLhccKhKWBSYoC31MdfxQKF7XY9nxukA7PRr7k0Zh0LquNMTxDDmT4/a9q4IyLgFe31FzJa+ApdYgW8dAWw==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.11.tgz", + "integrity": "sha512-AJd26OWXsHGc0xZIF7ARfAhpdE9lIvKTE3RTNohPqb2vW3V7XEiTBredgm8MH4R4YoF9gg8B2qYMIOqytZaONQ==", "dependencies": { "@bufbuild/protobuf": "^1.1.0" } @@ -16608,9 +16608,9 @@ } }, "@bufbuild/protobuf": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.1.tgz", - "integrity": "sha512-BUyJWutgP2S8K/1NphOJokuwDckXS4qI2T1pGZAlkFdZchWae3jm6fCdkcGbLlM1QLOcNFFePd+7Feo4BYGrJQ==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.3.tgz", + "integrity": "sha512-AoHSiIpTFF97SQgmQni4c+Tyr0CDhkaRaR2qGEJTEbauqQwLRpLrd9yVv//wVHOSxr/b4FJcL54VchhY6710xA==" }, "@discoveryjs/json-ext": { "version": "0.5.7", @@ -16956,9 +16956,9 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@porter-dev/api-contracts": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.8.tgz", - "integrity": "sha512-2eNrLhccKhKWBSYoC31MdfxQKF7XY9nxukA7PRr7k0Zh0LquNMTxDDmT4/a9q4IyLgFe31FzJa+ApdYgW8dAWw==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.11.tgz", + "integrity": "sha512-AJd26OWXsHGc0xZIF7ARfAhpdE9lIvKTE3RTNohPqb2vW3V7XEiTBredgm8MH4R4YoF9gg8B2qYMIOqytZaONQ==", "requires": { "@bufbuild/protobuf": "^1.1.0" } diff --git a/dashboard/package.json b/dashboard/package.json index 620bfdb193..601ec64277 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -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.2.8", + "@porter-dev/api-contracts": "^0.2.11", "@react-spring/web": "^9.6.1", "@sentry/react": "^6.13.2", "@sentry/tracing": "^6.13.2", diff --git a/dashboard/src/lib/porter-apps/services.ts b/dashboard/src/lib/porter-apps/services.ts index ff10f380f4..9440d0fe05 100644 --- a/dashboard/src/lib/porter-apps/services.ts +++ b/dashboard/src/lib/porter-apps/services.ts @@ -9,6 +9,7 @@ import { deserializeHealthCheck, domainsValidator, healthcheckValidator, + ingressAnnotationsValidator, serializeAutoscaling, SerializedAutoscaling, SerializedHealthcheck, @@ -18,6 +19,7 @@ import { serviceNumberValidator, serviceStringValidator, } from "./values"; +import _ from "lodash"; export type DetectedServices = { services: ClientService[]; @@ -37,6 +39,7 @@ const webConfigValidator = z.object({ domains: domainsValidator, healthCheck: healthcheckValidator.optional(), private: serviceBooleanValidator.optional(), + ingressAnnotations: ingressAnnotationsValidator.default([]), }); export type ClientWebConfig = z.infer; @@ -107,6 +110,7 @@ export type SerializedService = { autoscaling?: SerializedAutoscaling; healthCheck?: SerializedHealthcheck; private?: boolean; + ingressAnnotations: Record; } | { type: "worker"; @@ -174,6 +178,7 @@ export function defaultSerialized({ healthCheck: defaultHealthCheck, domains: [], private: false, + ingressAnnotations: {}, }, })) .with("worker", () => ({ @@ -225,6 +230,12 @@ export function serializeService(service: ClientService): SerializedService { domains: config.domains.map((domain) => ({ name: domain.name.value, })), + ingressAnnotations: Object.fromEntries( + config.ingressAnnotations.map((annotation) => [ + annotation.key, + annotation.value, + ]) + ), private: config.private?.value, }, }) @@ -325,6 +336,28 @@ export function deserializeService({ ]) ).map((domain) => ({ name: domain })); + const uniqueAnnotations = _.uniqBy( + [ + ...Object.entries(overrideWebConfig?.ingressAnnotations ?? {}).map( + (annotation) => { + return { + key: annotation[0], + value: annotation[1], + readOnly: true, + }; + } + ), + ...Object.entries(config.ingressAnnotations).map((annotation) => { + return { + key: annotation[0], + value: annotation[1], + readOnly: false, + }; + }), + ], + "key" + ); + return { ...baseService, config: { @@ -348,6 +381,7 @@ export function deserializeService({ )?.name ), })), + ingressAnnotations: uniqueAnnotations, private: typeof config.private === "boolean" || typeof overrideWebConfig?.private === "boolean" diff --git a/dashboard/src/lib/porter-apps/values.ts b/dashboard/src/lib/porter-apps/values.ts index eeebe791b3..45bb35b2c9 100644 --- a/dashboard/src/lib/porter-apps/values.ts +++ b/dashboard/src/lib/porter-apps/values.ts @@ -115,37 +115,45 @@ export function deserializeAutoscaling({ }: { autoscaling?: SerializedAutoscaling; override?: SerializedAutoscaling; - setDefaults: boolean; + setDefaults: boolean; }): ClientAutoscaling | undefined { - return ( - autoscaling ? { - enabled: ServiceField.boolean(autoscaling.enabled, override?.enabled), - minInstances: autoscaling.minInstances - ? ServiceField.number(autoscaling.minInstances, override?.minInstances) - : ServiceField.number(1, undefined), - maxInstances: autoscaling.maxInstances - ? ServiceField.number(autoscaling.maxInstances, override?.maxInstances) - : ServiceField.number(10, undefined), - cpuThresholdPercent: autoscaling.cpuThresholdPercent - ? ServiceField.number( - autoscaling.cpuThresholdPercent, - override?.cpuThresholdPercent - ) - : ServiceField.number(50, undefined), - memoryThresholdPercent: autoscaling.memoryThresholdPercent - ? ServiceField.number( - autoscaling.memoryThresholdPercent, - override?.memoryThresholdPercent - ) - : ServiceField.number(50, undefined), - } : (setDefaults ? { + return autoscaling + ? { + enabled: ServiceField.boolean(autoscaling.enabled, override?.enabled), + minInstances: autoscaling.minInstances + ? ServiceField.number( + autoscaling.minInstances, + override?.minInstances + ) + : ServiceField.number(1, undefined), + maxInstances: autoscaling.maxInstances + ? ServiceField.number( + autoscaling.maxInstances, + override?.maxInstances + ) + : ServiceField.number(10, undefined), + cpuThresholdPercent: autoscaling.cpuThresholdPercent + ? ServiceField.number( + autoscaling.cpuThresholdPercent, + override?.cpuThresholdPercent + ) + : ServiceField.number(50, undefined), + memoryThresholdPercent: autoscaling.memoryThresholdPercent + ? ServiceField.number( + autoscaling.memoryThresholdPercent, + override?.memoryThresholdPercent + ) + : ServiceField.number(50, undefined), + } + : setDefaults + ? { enabled: ServiceField.boolean(false, undefined), minInstances: ServiceField.number(1, undefined), maxInstances: ServiceField.number(10, undefined), cpuThresholdPercent: ServiceField.number(50, undefined), memoryThresholdPercent: ServiceField.number(50, undefined), - } : undefined ) - ); + } + : undefined; } // Health Check @@ -180,17 +188,19 @@ export function deserializeHealthCheck({ override?: SerializedHealthcheck; setDefaults: boolean; }): ClientHealthCheck | undefined { - return ( - health ? { - enabled: ServiceField.boolean(health.enabled, override?.enabled), - httpPath: health.httpPath - ? ServiceField.string(health.httpPath, override?.httpPath) - : ServiceField.string("", undefined), - } : (setDefaults ? { + return health + ? { + enabled: ServiceField.boolean(health.enabled, override?.enabled), + httpPath: health.httpPath + ? ServiceField.string(health.httpPath, override?.httpPath) + : ServiceField.string("", undefined), + } + : setDefaults + ? { enabled: ServiceField.boolean(false, undefined), httpPath: ServiceField.string("", undefined), - } : undefined) - ); + } + : undefined; } // Domains @@ -200,3 +210,15 @@ export const domainsValidator = z.array( }) ); export type ClientDomains = z.infer; + +// Ingress Annotations +export const ingressAnnotationsValidator = z.array( + z.object({ + key: z.string(), + value: z.string(), + readOnly: z.boolean(), + }) +); +export type ClientIngressAnnotations = z.infer< + typeof ingressAnnotationsValidator +>; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IngressCustomAnnotations.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IngressCustomAnnotations.tsx new file mode 100644 index 0000000000..0a3115d1ce --- /dev/null +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/IngressCustomAnnotations.tsx @@ -0,0 +1,107 @@ +import Button from "components/porter/Button"; +import { ControlledInput } from "components/porter/ControlledInput"; +import Spacer from "components/porter/Spacer"; +import { PorterAppFormData } from "lib/porter-apps"; +import React from "react"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import styled from "styled-components"; + +type Props = { + index: number; +}; + +const IngressCustomAnnotations: React.FC = ({ index }) => { + const { control, register } = useFormContext(); + const { remove, append, fields } = useFieldArray({ + control, + name: `app.services.${index}.config.ingressAnnotations`, + }); + + return ( +
+ {fields.length !== 0 + ? fields.map((annotation, i) => { + return ( + <> + + + + { + remove(i); + }} + > + cancel + + + + + ); + }) + : null} + +
+ ); +}; + +export default IngressCustomAnnotations; + +const AnnotationContainer = styled.div` + display: flex; + align-items: center; + gap: 5px; +`; + +const DeleteButton = styled.div` + width: 15px; + height: 15px; + display: flex; + align-items: center; + margin-left: 8px; + margin-top: -3px; + justify-content: center; + + > i { + font-size: 17px; + color: #ffffff44; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + :hover { + color: #ffffff88; + } + } +`; diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx index 0466c1460c..8c04cca4f3 100644 --- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx +++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Networking.tsx @@ -7,6 +7,7 @@ import { PorterAppFormData } from "lib/porter-apps"; import Checkbox from "components/porter/Checkbox"; import Text from "components/porter/Text"; import CustomDomains from "./CustomDomains"; +import IngressCustomAnnotations from "./IngressCustomAnnotations"; type NetworkingProps = { index: number; @@ -95,6 +96,7 @@ const Networking: React.FC = ({ index, service }) => { + )} diff --git a/go.mod b/go.mod index 4eec522a9f..0a668af8cc 100644 --- a/go.mod +++ b/go.mod @@ -82,7 +82,7 @@ require ( github.com/matryer/is v1.4.0 github.com/nats-io/nats.go v1.24.0 github.com/open-policy-agent/opa v0.44.0 - github.com/porter-dev/api-contracts v0.2.10 + github.com/porter-dev/api-contracts v0.2.11 github.com/riandyrn/otelchi v0.5.1 github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d diff --git a/go.sum b/go.sum index 2ca70c0c66..63dcb36e7c 100644 --- a/go.sum +++ b/go.sum @@ -1516,8 +1516,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw= -github.com/porter-dev/api-contracts v0.2.10 h1:xnnV3ffaLSajHMYyn96l49vLIY/J4lvZZgwKF+8aclk= -github.com/porter-dev/api-contracts v0.2.10/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= +github.com/porter-dev/api-contracts v0.2.11 h1:hwE0Pzn3P7LCaMOFXCMzSwIFaY+sABC96m2fOSl1E10= +github.com/porter-dev/api-contracts v0.2.11/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8= github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M= github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=