diff --git a/README.md b/README.md index 7f7f0520d..757e70493 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul - [📦 Demo](#-demo) - [🔗 User's guide](#-users-guide) -- [🛠️ Installation](#️-installation) +- [🛠️ Installation](#️-installation) +- [🚀 Deploying Checkmate with Helm](#-deploying-checkmate-with-helm) - [🏁 Translations](#-translations) - [🚀 Performance](#-performance) - [💚 Questions & Ideas](#-questions--ideas) @@ -50,7 +51,10 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). ## 🛠️ Installation -See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. +See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). + +Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. + ## 🏁 Translations diff --git a/charts/helm/checkmate/.helmignore b/charts/helm/checkmate/.helmignore new file mode 100644 index 000000000..21846e965 --- /dev/null +++ b/charts/helm/checkmate/.helmignore @@ -0,0 +1,17 @@ +.DS_Store +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +*.swp +*.bak +*.tmp +*.orig +*~ +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/helm/checkmate/Chart.yaml b/charts/helm/checkmate/Chart.yaml new file mode 100644 index 000000000..97f320e68 --- /dev/null +++ b/charts/helm/checkmate/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: checkmate-chart +description: A Helm chart for Checkmate App +type: application +version: 0.1.0 +appVersion: "2.3" diff --git a/charts/helm/checkmate/INSTALLATION.md b/charts/helm/checkmate/INSTALLATION.md new file mode 100644 index 000000000..9ca321ad3 --- /dev/null +++ b/charts/helm/checkmate/INSTALLATION.md @@ -0,0 +1,39 @@ +# Kubernetes Installation Guide for Checkmate + +This guide walks you through deploying Checkmate on your Kubernetes cluster using Helm. + +## Prerequisites + +- A running Kubernetes cluster +- Helm CLI installed and configured +- `kubectl` configured to access your cluster + +## Steps + +### 1. Clone the repo and navigate to the Helm chart + +```bash +git clone https://github.com/bluewave-labs/checkmate.git +cd checkmate/charts/helm/checkmate +``` + +### 2. Customize values.yaml +Edit `values.yaml` to update: +- `client.ingress.host` and `server.ingress.host` with your domain names +- `server.protocol` (usually http or https) +- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values + +### 3. Deploy the Helm chart +```bash +helm install checkmate ./charts/helm/checkmate +``` +This will deploy the client, server, MongoDB, and Redis components. + +### 4. Verify the deployment +Check pods and services: +```bash +kubectl get pods +kubectl get svc +``` + +Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts. diff --git a/charts/helm/checkmate/templates/_prechecks.yaml b/charts/helm/checkmate/templates/_prechecks.yaml new file mode 100644 index 000000000..870ab5c77 --- /dev/null +++ b/charts/helm/checkmate/templates/_prechecks.yaml @@ -0,0 +1,12 @@ +{{- if eq .Values.client.ingress.host "change_me" }} + {{- fail "client.ingress.host must be overridden and not set to 'change_me'" }} +{{- end }} + +{{- if eq .Values.server.ingress.host "change_me" }} + {{- fail "server.ingress.host must be overridden and not set to 'change_me'" }} +{{- end }} + +{{- $protocol := .Values.server.protocol }} +{{- if not (or (eq $protocol "http") (eq $protocol "https")) }} + {{- fail "server.protocol must be either 'http' or 'https'" }} +{{- end }} diff --git a/charts/helm/checkmate/templates/client-deployment.yaml b/charts/helm/checkmate/templates/client-deployment.yaml new file mode 100644 index 000000000..6c1f70fd7 --- /dev/null +++ b/charts/helm/checkmate/templates/client-deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client +spec: + replicas: 1 + selector: + matchLabels: + app: client + template: + metadata: + labels: + app: client + spec: + containers: + - name: client + image: {{ .Values.client.image }} + ports: + - containerPort: {{ .Values.client.port }} + env: + - name: UPTIME_APP_API_BASE_URL + value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1" diff --git a/charts/helm/checkmate/templates/client-ingress.yaml b/charts/helm/checkmate/templates/client-ingress.yaml new file mode 100644 index 000000000..da059a863 --- /dev/null +++ b/charts/helm/checkmate/templates/client-ingress.yaml @@ -0,0 +1,20 @@ +{{- if .Values.client.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: client-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - host: {{ .Values.client.ingress.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: client + port: + number: {{ .Values.client.port }} +{{- end }} diff --git a/charts/helm/checkmate/templates/client-service.yaml b/charts/helm/checkmate/templates/client-service.yaml new file mode 100644 index 000000000..afb88188c --- /dev/null +++ b/charts/helm/checkmate/templates/client-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: client +spec: + selector: + app: client + ports: + - port: {{ .Values.client.port }} diff --git a/charts/helm/checkmate/templates/mongodb-deployment.yaml b/charts/helm/checkmate/templates/mongodb-deployment.yaml new file mode 100644 index 000000000..4c906fe09 --- /dev/null +++ b/charts/helm/checkmate/templates/mongodb-deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongodb +spec: + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + containers: + - name: mongodb + image: {{ .Values.mongodb.image }} + ports: + - containerPort: {{ .Values.mongodb.port }} + command: ["mongod", "--quiet", "--bind_ip_all"] + volumeMounts: + - name: mongodb-data + mountPath: /data/db + volumes: + - name: mongodb-data + persistentVolumeClaim: + claimName: mongodb-pvc \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-pvc.yaml b/charts/helm/checkmate/templates/mongodb-pvc.yaml new file mode 100644 index 000000000..435b662e6 --- /dev/null +++ b/charts/helm/checkmate/templates/mongodb-pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mongodb-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.mongodbSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-service.yaml b/charts/helm/checkmate/templates/mongodb-service.yaml new file mode 100644 index 000000000..2911dc3eb --- /dev/null +++ b/charts/helm/checkmate/templates/mongodb-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongodb +spec: + selector: + app: mongodb + ports: + - port: {{ .Values.mongodb.port }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-deployment.yaml b/charts/helm/checkmate/templates/redis-deployment.yaml new file mode 100644 index 000000000..cc9ffdaf9 --- /dev/null +++ b/charts/helm/checkmate/templates/redis-deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: {{ .Values.redis.image }} + ports: + - containerPort: {{ .Values.redis.port }} + volumeMounts: + - name: redis-data + mountPath: /data + volumes: + - name: redis-data + persistentVolumeClaim: + claimName: redis-pvc diff --git a/charts/helm/checkmate/templates/redis-pvc.yaml b/charts/helm/checkmate/templates/redis-pvc.yaml new file mode 100644 index 000000000..2f0d81960 --- /dev/null +++ b/charts/helm/checkmate/templates/redis-pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: redis-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.redisSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-service.yaml b/charts/helm/checkmate/templates/redis-service.yaml new file mode 100644 index 000000000..f29fa46de --- /dev/null +++ b/charts/helm/checkmate/templates/redis-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + selector: + app: redis + ports: + - port: {{ .Values.redis.port }} diff --git a/charts/helm/checkmate/templates/secrets.yaml b/charts/helm/checkmate/templates/secrets.yaml new file mode 100644 index 000000000..42e7c6047 --- /dev/null +++ b/charts/helm/checkmate/templates/secrets.yaml @@ -0,0 +1,19 @@ +{{- $secrets := .Values.secrets }} + +{{- if or (not $secrets.JWT_SECRET) (eq $secrets.JWT_SECRET "change_me") }} + {{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }} +{{- end }} + +{{- if or (not $secrets.REFRESH_TOKEN_SECRET) (eq $secrets.REFRESH_TOKEN_SECRET "change_me") }} + {{- fail "secrets.REFRESH_TOKEN_SECRET must be overridden and cannot be 'change_me'" }} +{{- end }} + +apiVersion: v1 +kind: Secret +metadata: + name: checkmate-secrets +type: Opaque +stringData: +{{- range $key, $value := := $secrets }} + {{ $key }}: {{ $value | quote }} +{{- end }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/server-deployment.yaml b/charts/helm/checkmate/templates/server-deployment.yaml new file mode 100644 index 000000000..8548495dd --- /dev/null +++ b/charts/helm/checkmate/templates/server-deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: server +spec: + replicas: 1 + selector: + matchLabels: + app: server + template: + metadata: + labels: + app: server + spec: + containers: + - name: server + image: {{ .Values.server.image }} + ports: + - containerPort: {{ .Values.server.port }} + envFrom: + - secretRef: + name: checkmate-secrets + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket diff --git a/charts/helm/checkmate/templates/server-ingress.yaml b/charts/helm/checkmate/templates/server-ingress.yaml new file mode 100644 index 000000000..acf64f95f --- /dev/null +++ b/charts/helm/checkmate/templates/server-ingress.yaml @@ -0,0 +1,20 @@ +{{- if .Values.server.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: server-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - host: {{ .Values.server.ingress.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: server + port: + number: {{ .Values.server.port }} +{{- end }} diff --git a/charts/helm/checkmate/templates/server-service.yaml b/charts/helm/checkmate/templates/server-service.yaml new file mode 100644 index 000000000..64494ff90 --- /dev/null +++ b/charts/helm/checkmate/templates/server-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: server +spec: + selector: + app: server + ports: + - port: {{ .Values.server.port }} diff --git a/charts/helm/checkmate/values.yaml b/charts/helm/checkmate/values.yaml new file mode 100644 index 000000000..79c77a49e --- /dev/null +++ b/charts/helm/checkmate/values.yaml @@ -0,0 +1,41 @@ +client: + image: ghcr.io/bluewave-labs/checkmate-client:v2.3 + port: 80 + ingress: + enabled: true + host: change_me + +server: + image: ghcr.io/bluewave-labs/checkmate-backend:v2.3 + port: 52345 + protocol: change_me + ingress: + enabled: true + host: change_me + +redis: + image: redis:7.2 + port: 6379 + +mongodb: + image: mongo:7.0 + port: 27017 + +secrets: + JWT_SECRET: change_me + REFRESH_TOKEN_SECRET: change_me + SYSTEM_EMAIL_ADDRESS: test@example.com + SYSTEM_EMAIL_PASSWORD: change_me + SYSTEM_EMAIL_HOST: smtp.example.com + SYSTEM_EMAIL_PORT: "587" + PAGESPEED_API_KEY: change_me + DB_CONNECTION_STRING: mongodb://mongodb:27017/uptime_db + REDIS_HOST: redis + REDIS_PORT: "6379" + DB_TYPE: MongoDB + TOKEN_TTL: 99d + REFRESH_TOKEN_TTL: 99d + +persistence: + mongodbSize: 5Gi + redisSize: 1Gi \ No newline at end of file diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index d59d88c74..4f0e9c049 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -205,6 +205,10 @@ const useFetchStatsByMonitorId = ({ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { + if (typeof monitorId === "undefined") { + setIsLoading(false); + return; + } const fetchMonitor = async () => { try { setIsLoading(true); diff --git a/client/src/Pages/PageSpeed/Configure/index.css b/client/src/Pages/PageSpeed/Configure/index.css deleted file mode 100644 index 89a7c3466..000000000 --- a/client/src/Pages/PageSpeed/Configure/index.css +++ /dev/null @@ -1,9 +0,0 @@ -.configure-pagespeed button { - height: var(--env-var-height-2); -} - -.configure-pagespeed .field, -.configure-pagespeed .section-disabled, -.configure-pagespeed .select-wrapper { - flex: 1; -} diff --git a/client/src/Pages/PageSpeed/Configure/index.jsx b/client/src/Pages/PageSpeed/Configure/index.jsx deleted file mode 100644 index 71e1e111e..000000000 --- a/client/src/Pages/PageSpeed/Configure/index.jsx +++ /dev/null @@ -1,351 +0,0 @@ -// Components -import { Box, Stack, Tooltip, Typography, Button } from "@mui/material"; -import ConfigBox from "../../../Components/ConfigBox"; -import Select from "../../../Components/Inputs/Select"; -import TextInput from "../../../Components/Inputs/TextInput"; -import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; -import SkeletonLayout from "./skeleton"; -import NotificationsConfig from "../../../Components/NotificationConfig"; -import Dialog from "../../../Components/Dialog"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useParams } from "react-router"; -import { monitorValidation } from "../../../Validation/validation"; -import { useTranslation } from "react-i18next"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; -import { - useFetchMonitorById, - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, -} from "../../../Hooks/monitorHooks"; -const PageSpeedConfigure = () => { - // Redux state - - // Local state - const [monitor, setMonitor] = useState({}); - const [errors, setErrors] = useState({}); - const [isOpen, setIsOpen] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - const MS_PER_MINUTE = 60000; - const { monitorId } = useParams(); - const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); - - const [notifications, notificationsAreLoading, notificationsError] = - useGetNotificationsByTeamId(); - - const [isLoading] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger }); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - const [pauseMonitor, isPausing] = usePauseMonitor(); - - const frequencies = [ - { _id: 3, name: "3 minutes" }, - { _id: 5, name: "5 minutes" }, - { _id: 10, name: "10 minutes" }, - { _id: 20, name: "20 minutes" }, - { _id: 60, name: "1 hour" }, - { _id: 1440, name: "1 day" }, - { _id: 10080, name: "1 week" }, - ]; - - // Handlers - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const onChange = (event) => { - let { value, name } = event.target; - - if (name === "interval") { - value = value * MS_PER_MINUTE; - } - setMonitor((prev) => ({ - ...prev, - [name]: value, - })); - - const validation = monitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - - setErrors((prev) => { - const updatedErrors = { ...prev }; - - if (validation.error) updatedErrors[name] = validation.error.details[0].message; - else delete updatedErrors[name]; - return updatedErrors; - }); - }; - - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - - const onSubmit = async (event) => { - event.preventDefault(); - await updateMonitor({ monitor, redirect: "/pagespeed" }); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/pagespeed" }); - }; - - return ( - - {Object.keys(monitor).length === 0 ? ( - - ) : ( - <> - - - - - - {monitor.name} - - - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - - - - - - - - - - {t("settingsGeneralSettings")} - - - {t("pageSpeedConfigureSettingsDescription")} - - - .Mui-disabled)": { - backgroundColor: theme.palette.tertiary.main, - }, - }} - > - - - - - - - {t("notificationConfig.title")} - - {t("notificationConfig.description")} - - - - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - + {!isCreate && ( + setIsOpen(false)} + confirmationButtonLabel={t("delete")} + onConfirm={handleRemove} + /> + )} ); }; -export default CreatePageSpeed; + +export default PageSpeedSetup; diff --git a/client/src/Pages/PageSpeed/Configure/skeleton.jsx b/client/src/Pages/PageSpeed/Create/skeleton.jsx similarity index 100% rename from client/src/Pages/PageSpeed/Configure/skeleton.jsx rename to client/src/Pages/PageSpeed/Create/skeleton.jsx diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index f3342e101..6e6f6caba 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -21,7 +21,7 @@ import MonitorCountHeader from "../../../Components/MonitorCountHeader"; import { Stack, Box, Button, Typography } from "@mui/material"; // Utils -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect } from "react"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; @@ -146,6 +146,12 @@ const UptimeMonitors = () => { monitorUpdateTrigger, }); + useEffect(() => { + if (isSearching) { + setPage(undefined); + } + }, [isSearching]); + const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading; if (networkError) { return ( diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 965ed2676..3db8b772d 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -18,9 +18,8 @@ import UptimeConfigure from "../Pages/Uptime/Configure"; // PageSpeed import PageSpeed from "../Pages/PageSpeed/Monitors"; -import PageSpeedCreate from "../Pages/PageSpeed/Create"; import PageSpeedDetails from "../Pages/PageSpeed/Details"; -import PageSpeedConfigure from "../Pages/PageSpeed/Configure"; +import PageSpeedCreate from "../Pages/PageSpeed/Create"; // Infrastructure import Infrastructure from "../Pages/Infrastructure/Monitors"; @@ -107,7 +106,7 @@ const Routes = () => { /> } + element={} />