Просмотр исходного кода

custom nginx ingress annotations on web svcs (#3765)

ianedwards 2 лет назад
Родитель
Сommit
a0f69aecfd

+ 13 - 13
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"
       }

+ 1 - 1
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",

+ 34 - 0
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<typeof webConfigValidator>;
 
@@ -107,6 +110,7 @@ export type SerializedService = {
         autoscaling?: SerializedAutoscaling;
         healthCheck?: SerializedHealthcheck;
         private?: boolean;
+        ingressAnnotations: Record<string, string>;
       }
     | {
         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"

+ 56 - 34
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<typeof domainsValidator>;
+
+// 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
+>;

+ 107 - 0
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<Props> = ({ index }) => {
+  const { control, register } = useFormContext<PorterAppFormData>();
+  const { remove, append, fields } = useFieldArray({
+    control,
+    name: `app.services.${index}.config.ingressAnnotations`,
+  });
+
+  return (
+    <div>
+      {fields.length !== 0
+        ? fields.map((annotation, i) => {
+            return (
+              <>
+                <AnnotationContainer key={i}>
+                  <ControlledInput
+                    type="text"
+                    placeholder="kubernetes.io/ingress.class"
+                    disabled={annotation.readOnly}
+                    width="275px"
+                    disabledTooltip={
+                      "You may only edit this field in your porter.yaml."
+                    }
+                    {...register(
+                      `app.services.${index}.config.ingressAnnotations.${i}.key`
+                    )}
+                  />
+                  <ControlledInput
+                    type="text"
+                    placeholder="nginx"
+                    disabled={annotation.readOnly}
+                    width="275px"
+                    disabledTooltip={
+                      "You may only edit this field in your porter.yaml."
+                    }
+                    {...register(
+                      `app.services.${index}.config.ingressAnnotations.${i}.value`
+                    )}
+                  />
+                  <DeleteButton
+                    onClick={() => {
+                      remove(i);
+                    }}
+                  >
+                    <i className="material-icons">cancel</i>
+                  </DeleteButton>
+                </AnnotationContainer>
+                <Spacer y={0.25} />
+              </>
+            );
+          })
+        : null}
+      <Button
+        onClick={() => {
+          append({
+            key: "",
+            value: "",
+            readOnly: false,
+          });
+        }}
+      >
+        + Add Annotation
+      </Button>
+    </div>
+  );
+};
+
+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;
+    }
+  }
+`;

+ 2 - 0
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<NetworkingProps> = ({ index, service }) => {
           <Spacer y={0.5} />
           <CustomDomains index={index} />
           <Spacer y={0.5} />
+          <IngressCustomAnnotations index={index} />
         </>
       )}
     </>

+ 1 - 1
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

+ 2 - 2
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=