diff --git a/api/package.json b/api/package.json index feda064..36b24c3 100644 --- a/api/package.json +++ b/api/package.json @@ -24,6 +24,7 @@ "express": "^4.17.2", "jsonwebtoken": "^8.5.1", "mongoose": "^6.2.0", + "morgan": "^1.10.0", "passport": "^0.5.2", "passport-jwt": "^4.0.0", "socket.io": "^4.4.1" diff --git a/api/src/models/Service.js b/api/src/models/Service.js index 22fc512..c7a76d0 100644 --- a/api/src/models/Service.js +++ b/api/src/models/Service.js @@ -28,6 +28,11 @@ const ServiceSchema = new mongoose.Schema({ type: [{ from: String, to: String }], required: false, }, + environments: { + type: [{ key: String, value: String }], + required: false, + default: [], + }, order: { type: Number, required: true, @@ -77,6 +82,14 @@ ServiceSchema.methods.getServiceLabels = function () { return labels; }; +ServiceSchema.methods.getEnvironments = function () { + const environments = this.environments.map((environment) => { + return `${environment.key}=${environment.value}`; + }); + + return environments; +}; + const transformHostsToLabel = (service) => { const label = `traefik.http.routers.${service.name}.rule`; const hosts = service.hosts.map((host) => { diff --git a/api/src/routes/services.js b/api/src/routes/services.js index 083697c..d3254e0 100644 --- a/api/src/routes/services.js +++ b/api/src/routes/services.js @@ -124,13 +124,18 @@ router.put("/:name", async (req, res) => { service.redirects = redirects; } + const environments = updateRequest.environments; + if (environments) { + service.environments = environments; + } + const tag = updateRequest.tag; if (tag) { service.tag = tag; } await service.save(); - if (hosts || image || redirects || tag) { + if (hosts || image || redirects || environments || tag) { await updateContainer(service, service.image); await startContainer(service); } diff --git a/api/src/server.js b/api/src/server.js index 4a7be0e..f19e58a 100644 --- a/api/src/server.js +++ b/api/src/server.js @@ -3,6 +3,7 @@ const path = require("path"); const dotenv = require("dotenv"); const passport = require("passport"); const cors = require("cors"); +const morgan = require("morgan"); const connectDB = require("./config/db"); const setupPassport = require("./config/passport"); @@ -24,6 +25,7 @@ app.use( }) ); +app.use(morgan("common")); app.use(passport.initialize()); app.use(express.json()); diff --git a/api/src/utils/docker.js b/api/src/utils/docker.js index 3d724d0..5ecb2b5 100644 --- a/api/src/utils/docker.js +++ b/api/src/utils/docker.js @@ -75,27 +75,59 @@ const updateContainer = async (service, image) => { containerLabels[element.split("=")[0]] = element.split("=")[1]; }); + const environments = service.getEnvironments(); + + const portBindings = {}; + if (oldContainer.Ports.length > 0) { + oldContainer.Ports.forEach((port) => { + const { PrivatePort, PublicPort, Type } = port; + if (PrivatePort === undefined || PublicPort === undefined) { + return; + } + portBindings[`${PrivatePort}/${Type}`] = [ + { + HostPort: `${PublicPort}`, + }, + ]; + }); + } + + const hostConfig = { + NetworkMode: service.network, + }; + + if (portBindings && Object.keys(portBindings).length > 0) { + hostConfig.PortBindings = portBindings; + } + Object.keys(containerLabels).map((key) => { if (!containerLabels[key] || containerLabels[key] === "") { delete containerLabels[key]; } }); - await deleteContainer(service); const container = await docker.createContainer({ Image: image.resolvedName, name: service.name, Labels: containerLabels, - HostConfig: { - NetworkMode: service.network, - }, + HostConfig: hostConfig, + Env: environments, }); service.containerId = container.id; service.status = "created"; await service.save(); }; +const getContainer = (containerId) => { + return docker.getContainer(containerId); +}; + +const inspectContainer = async (containerId) => { + const container = docker.getContainer(containerId); + return await container.inspect(); +}; + const getAllContainers = async () => { const containers = await docker.listContainers({ all: true, @@ -117,4 +149,6 @@ module.exports = { updateContainer, getAllContainers, getAllImages, + getContainer, + inspectContainer, }; diff --git a/api/src/utils/services.js b/api/src/utils/services.js index e63de9c..0566854 100644 --- a/api/src/utils/services.js +++ b/api/src/utils/services.js @@ -4,7 +4,6 @@ const { deleteContainer } = require("./docker"); const getOrCreateImage = async (resolvedName) => { const { repository, imageName, tag } = parseResolvedName(resolvedName); - if (!imageName || !tag || !repository) { return -1; } @@ -54,11 +53,11 @@ const createService = async (serviceRequest, image) => { }; const parseResolvedName = (resolvedName) => { - const regex = /^(.+)\/(.+):(.+)$|^(.+):(.+)$|^(.+)/; + const regex = /^(.+)\/(.+):(.+)$|^(.+):(.+)$|^(.+)\/(.+)|^(.+)$/; const match = regex.exec(resolvedName); - const repository = match[1] ?? "_"; - const imageName = match[2] ?? match[4] ?? match[6]; + const repository = match[1] ?? match[6] ?? "_"; + const imageName = match[2] ?? match[4] ?? match[7] ?? match[8]; const tag = match[3] ?? match[5] ?? "latest"; return { diff --git a/api/src/utils/startup.js b/api/src/utils/startup.js index 7186c35..7ada9d3 100644 --- a/api/src/utils/startup.js +++ b/api/src/utils/startup.js @@ -8,7 +8,11 @@ const { parseRedirectLabels, parseTraefikerLabels, } = require("./services"); -const { getAllContainers, getAllImages } = require("./docker"); +const { + getAllContainers, + getAllImages, + inspectContainer, +} = require("./docker"); const createImages = async () => { const images = await getAllImages(); @@ -41,9 +45,18 @@ const createContainers = async (images) => { ) { return Promise.resolve(); } - const image = images.find( - (image) => image.resolvedName === container.Image - ); + const image = images.find((image) => { + const { repository, imageName, tag } = parseResolvedName( + container.Image + ); + if ( + image.repository === repository && + image.name === imageName && + image.tag === tag + ) { + return true; + } + }); if (image === undefined) { return Promise.resolve(); @@ -59,12 +72,19 @@ const createContainers = async (images) => { const redirects = parseRedirectLabels(labels); const traefikerLabels = parseTraefikerLabels(labels); + const inspectData = await inspectContainer(container.Id); + const environments = inspectData.Config.Env.map((env) => { + const [key, value] = env.split("="); + return { key: key, value: value }; + }); + const service = new Service({ name: serviceName, status: container.State == "running" ? "running" : "stopped", image: image._id, hosts: hosts, redirects: redirects, + environments, order: i, tag: traefikerLabels.tag === "" ? serviceName : traefikerLabels.tag, containerId: container.Id, diff --git a/api/yarn.lock b/api/yarn.lock index 2d6ef83..6a42926 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -178,6 +178,13 @@ base64id@2.0.0, base64id@~2.0.0: resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -480,6 +487,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -1317,6 +1329,17 @@ mongoose@^6.2.0: regexp-clone "1.0.0" sift "13.5.2" +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + mpath@0.8.4: version "0.8.4" resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.8.4.tgz#6b566d9581621d9e931dd3b142ed3618e7599313" @@ -1404,6 +1427,11 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1628,6 +1656,11 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" diff --git a/client/src/atoms/atoms.ts b/client/src/atoms/atoms.ts index 6b8f4b6..d11bed8 100644 --- a/client/src/atoms/atoms.ts +++ b/client/src/atoms/atoms.ts @@ -32,10 +32,10 @@ export const loadingFlagsState = atom({ }, }); -export const redirectsModalState = atom({ - key: "isAddingRedirects", +export const settingsModalState = atom({ + key: "isEditingSettings", default: { - isAddingRedirects: false, + isEditingSettings: false, service: {}, }, }); diff --git a/client/src/components/dashboard/table/DashboardTable.tsx b/client/src/components/dashboard/table/DashboardTable.tsx index dd2137f..1a8a5e1 100644 --- a/client/src/components/dashboard/table/DashboardTable.tsx +++ b/client/src/components/dashboard/table/DashboardTable.tsx @@ -6,10 +6,10 @@ function DashboardTable() { { name: "Service Tag", screenReaderOnly: false }, { name: "Image Name", screenReaderOnly: false }, { name: "Service Hosts", screenReaderOnly: false }, - { name: "Add Redirects", screenReaderOnly: true }, { name: "Run/Stop", screenReaderOnly: true }, { name: "Edit", screenReaderOnly: true }, { name: "Delete", screenReaderOnly: true }, + { name: "Config", screenReaderOnly: true }, { name: "Order", screenReaderOnly: true }, ]; diff --git a/client/src/components/dashboard/table/DashboardTableBody.tsx b/client/src/components/dashboard/table/DashboardTableBody.tsx index a5d84fd..5214d0f 100644 --- a/client/src/components/dashboard/table/DashboardTableBody.tsx +++ b/client/src/components/dashboard/table/DashboardTableBody.tsx @@ -4,7 +4,7 @@ import { useRecoilState } from "recoil"; import { isCreatingServiceState, loadingFlagsState, - redirectsModalState, + settingsModalState, servicesState, } from "../../../atoms/atoms"; @@ -50,7 +50,7 @@ function DashboardTableBody({ columns }: Props) { isCreatingServiceState ); const [loadingFlags, setLoadingFlags] = useRecoilState(loadingFlagsState); - const [, setRedirectsModalOptions] = useRecoilState(redirectsModalState); + const [, setSettingsModalOptions] = useRecoilState(settingsModalState); const [serviceUnderEditing, setServiceUnderEditing] = useState< Service | undefined @@ -136,8 +136,8 @@ function DashboardTableBody({ columns }: Props) { }; const redirectsClicked = (service: Service) => { - setRedirectsModalOptions({ - isAddingRedirects: true, + setSettingsModalOptions({ + isEditingSettings: true, service: service, }); }; diff --git a/client/src/components/dashboard/table/DashboardTableRow.tsx b/client/src/components/dashboard/table/DashboardTableRow.tsx index 047a391..9b4b2df 100644 --- a/client/src/components/dashboard/table/DashboardTableRow.tsx +++ b/client/src/components/dashboard/table/DashboardTableRow.tsx @@ -1,6 +1,6 @@ import { Service } from "../../../types/Service"; import { Draggable } from "react-beautiful-dnd"; -import { MenuIcon, SwitchHorizontalIcon } from "@heroicons/react/solid"; +import { CogIcon, MenuIcon } from "@heroicons/react/solid"; import seedrandom from "seedrandom"; interface Props { @@ -158,21 +158,7 @@ function DashboardTableRow({ ))} - - - + - + + +
diff --git a/client/src/components/service-settings/ServiceSettingsModal.tsx b/client/src/components/service-settings/ServiceSettingsModal.tsx index 959edd6..75a8e76 100644 --- a/client/src/components/service-settings/ServiceSettingsModal.tsx +++ b/client/src/components/service-settings/ServiceSettingsModal.tsx @@ -2,25 +2,29 @@ import { Dialog } from "@headlessui/react"; import axios from "axios"; import { useEffect, useState } from "react"; import { useRecoilState, useRecoilValue } from "recoil"; -import { redirectsModalState } from "../../atoms/atoms"; +import { settingsModalState } from "../../atoms/atoms"; import { Service } from "../../types/Service"; import { Redirect } from "../../types/Redirect"; -import UrlRedirectsTable from "./table/UrlRedirectsTable"; import { updateService } from "../../utils/api"; +import Environment from "../../types/Environment"; +import EnvTable from "./env-table/EnvTable"; +import RedirsTable from "./redirs-table/RedirsTable"; function ServiceSettingsModal() { - const [redirectsModalOptions, setRedirectsModalOptions] = - useRecoilState(redirectsModalState); + const [settingsModalOptions, setSettingsModalOptions] = + useRecoilState(settingsModalState); const [service, setService] = useState( - redirectsModalOptions.service + settingsModalOptions.service ); - const [redirects, setRedirects] = useState(); + const [redirects, setRedirects] = useState([]); + const [environments, setEnvironments] = useState([]); useEffect(() => { - setService(redirectsModalOptions.service); - setRedirects(redirectsModalOptions.service.redirects); - }, [redirectsModalOptions.service]); + setService(settingsModalOptions.service); + setRedirects(settingsModalOptions.service.redirects ?? []); + setEnvironments(settingsModalOptions.service.environments ?? []); + }, [settingsModalOptions.service]); const saveClicked = async () => { closeModal(); @@ -31,53 +35,31 @@ function ServiceSettingsModal() { to: redirect.to, }; }); + + const noIdsEnvironments = environments!.map((environment) => { + return { + key: environment.key, + value: environment.value, + }; + }); + await updateService({ ...service, redirects: noIdsRedirects, + environments: noIdsEnvironments, }); }; const closeModal = () => { - setRedirectsModalOptions((state) => ({ + setSettingsModalOptions((state) => ({ ...state, - isAddingRedirects: false, + isEditingSettings: false, })); }; - const addNewRedirect = () => { - setRedirects((prevRedirects) => { - return [ - ...prevRedirects!, - { - _id: prevRedirects ? `${prevRedirects.length + 1}` : "0", - from: "", - to: "", - }, - ]; - }); - }; - - const updateRedirect = (redirect: Redirect) => { - setRedirects((prevRedirects) => { - return prevRedirects!.map((prevRedirect) => { - if (prevRedirect._id == redirect._id) { - return redirect; - } - return prevRedirect; - }); - }); - }; - - const deleteRedirect = (redirect: Redirect) => { - const newRedirects = redirects!.filter((prevRedirect) => { - return prevRedirect._id !== redirect._id; - }); - setRedirects(newRedirects); - }; - return ( {}} className="fixed inset-0 z-10 overflow-y-auto" > @@ -86,11 +68,17 @@ function ServiceSettingsModal() { {service !== undefined ? (
- { + setRedirects(data); + }} + /> + { + setEnvironments(data); + }} />