Skip to content

Commit

Permalink
chore: build docker image (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
MattKetmo authored Nov 8, 2024
1 parent 329ae11 commit 0a8c565
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 26 deletions.
98 changes: 98 additions & 0 deletions .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Docker

on:
push:
branches:
- main
paths:
- 'src/**'
- '*.ts'
- '*.json'
- '*.yaml'
- Dockerfile
- .github/workflows/docker.yaml
pull_request:
branches:
- main
paths:
- 'src/**'
- '*.ts'
- '*.json'
- '*.yaml'
- Dockerfile
- .github/workflows/docker.yaml
release:
types:
- published

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
PLATFORMS: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Set up QEMU
uses: docker/setup-qemu-action@v2

- name: "Generate Build ID (main)"
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
branch=${GITHUB_REF##*/}
sha=${GITHUB_SHA::8}
ts=$(date +%s)
echo "BUILD_ID=${branch}-${ts}-${sha}" >> $GITHUB_ENV
- name: "Generate Build ID (PR)"
if: github.event_name == 'pull_request'
run: |
echo "BUILD_ID=pr-${{ github.event.number }}-$GITHUB_RUN_ID" >> $GITHUB_ENV
- name: "Generate Build ID (Release)"
if: github.event_name == 'release'
run: |
echo "BUILD_ID=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=pr
type=ref,event=branch
type=raw,value=${{ env.BUILD_ID }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ env.PLATFORMS }}
cache-from: type=gha
cache-to: type=gha,mode=max
70 changes: 70 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# syntax=docker.io/docker/dockerfile:1

FROM node:22-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED=1

RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
output: "standalone",
};

export default nextConfig;
3 changes: 3 additions & 0 deletions public/humans.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* TEAM */
Name: Matthieu Moquet
Contact: matthieu [at] moquet [dot] net
6 changes: 0 additions & 6 deletions src/app/alerts/[handle]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { AlertsTemplate } from "@/components/alerts/template"
import { config } from "@/config"
import { notFound } from "next/navigation"

type Props = {
params: Promise<{ handle: string }>
Expand All @@ -9,10 +7,6 @@ type Props = {
export default async function AlertsViewPage(props: Props) {
const { handle } = await props.params

if (!config.views[handle]) {
return notFound()
}

return (
<AlertsTemplate view={handle} />
)
Expand Down
3 changes: 2 additions & 1 deletion src/app/api/clusters/[slug]/alerts/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { config } from "@/config";
import { getConfig } from "@/config";

export async function GET(
_: Request,
{ params }: { params: Promise<{ slug: string }> }
) {
const cluster = (await params).slug
const config = await getConfig()

const endpoint = config.clusters.find((c) => c.name === cluster)?.endpoint
if (!endpoint) {
Expand Down
13 changes: 12 additions & 1 deletion src/app/api/config/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { clientConfig } from "@/config/client";
import { getConfig } from "@/config";

export async function GET(_: Request) {
const config = await getConfig()

// Remove the endpoints from the client config
const clientConfig = {
...config,
clusters: config.clusters.map((c) => ({
...c,
endpoint: ""
})),
}

return new Response(JSON.stringify(clientConfig), {
headers: { "Content-Type": "application/json" },
});
Expand Down
7 changes: 4 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { clientConfig } from "@/config/client";
import { ConfigProvider } from "@/contexts/config";
import { ThemeProvider } from "next-themes";
import { AppLayout } from "@/components/layout/app-layout";
import { AlertsProvider } from "@/contexts/alerts";

export const dynamic = "force-dynamic";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
Expand All @@ -23,7 +24,7 @@ export const metadata: Metadata = {
description: "Dashboard for AlertManager Prometheus",
};

export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
Expand All @@ -39,7 +40,7 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<ConfigProvider config={clientConfig}>
<ConfigProvider>
<AlertsProvider>
<AppLayout>
{children}
Expand Down
6 changes: 5 additions & 1 deletion src/components/alerts/template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AlertModal } from './alert-modal'
import { LabelFilter, Group } from './types'
import { useConfig } from '@/contexts/config'
import { alertFilter, alertSort } from './utils'

import { notFound } from 'next/navigation'

type Props = {
view: string
Expand All @@ -22,6 +22,10 @@ export function AlertsTemplate(props: Props) {
const { alerts, loading, errors, refreshAlerts } = useAlerts()
const [selectedAlert, setSelectedAlert] = useState<Alert | null>(null)

if (!config.views[view]) {
return notFound()
}

const { filters, groupBy } = config.views[view]

// Flatten alerts
Expand Down
2 changes: 1 addition & 1 deletion src/config/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { config } from "."
import { config } from "./server"

// Remove the endpoints from the client config
export const clientConfig = {
Expand Down
38 changes: 28 additions & 10 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseConfigFile, resolveConfigFile, resolveEnvVars} from './utils'
import { parseConfigFile, resolveConfigFile, resolveEnvVars } from './utils'
import { Config, ConfigSchema } from './types'

const defaultConfigFiles = [
Expand All @@ -7,16 +7,34 @@ const defaultConfigFiles = [
'config.yml',
]

const configPath = process.env.APP_CONFIG || await resolveConfigFile(defaultConfigFiles, process.cwd())
/* eslint-disable @typescript-eslint/no-explicit-any */
const memoize = <T extends (...args: any[]) => Promise<any>>(fn: T) => {
const cache = new Map<string, ReturnType<T>>()
return async function(...args: Parameters<T>): Promise<ReturnType<T>> {
const key = JSON.stringify(args)
if (cache.has(key)) {
return cache.get(key) as ReturnType<T>
}
const result = await fn(...args)
cache.set(key, result)
return result
}
}

console.log(`Using config file: ${configPath}`)
export const getConfig = memoize(async function() {
const configPath = process.env.APP_CONFIG || await resolveConfigFile(defaultConfigFiles, process.cwd())

const parsedConfig = await parseConfigFile(configPath)
const resolvedConfig = resolveEnvVars(parsedConfig)
console.log(`Using config file: ${configPath}`)

const validatedConfig = ConfigSchema.safeParse(resolvedConfig)
if (!validatedConfig.success) {
throw new Error('Invalid config file: ' + validatedConfig.error.errors)
}
const parsedConfig = await parseConfigFile(configPath)
const resolvedConfig = resolveEnvVars(parsedConfig)

const validatedConfig = ConfigSchema.safeParse(resolvedConfig)
if (!validatedConfig.success) {
throw new Error('Invalid config file: ' + validatedConfig.error.errors)
}

const config: Config = validatedConfig.data

export const config: Config = validatedConfig.data
return config
})
3 changes: 3 additions & 0 deletions src/config/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { getConfig } from "."

export const config = await getConfig()
33 changes: 31 additions & 2 deletions src/contexts/config.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import React, { createContext, useContext, ReactNode } from 'react'
import React, { createContext, useContext, ReactNode, useState, useEffect } from 'react'
import { Config } from '@/config/types'

interface ConfigContextProps {
Expand All @@ -9,7 +9,36 @@ interface ConfigContextProps {

const ConfigContext = createContext<ConfigContextProps | undefined>(undefined)

export const ConfigProvider = ({ children, config }: { children: ReactNode, config: Config }) => {
export const ConfigProvider = ({ children }: { children: ReactNode }) => {
const [config, setConfig] = useState<Config | null>(null)
const [error, setError] = useState<string | null>(null)

useEffect(() => {
const fetchConfig = async () => {
try {
const response = await fetch('/api/config')
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await response.json()
setConfig(data)
} catch (error) {
console.error('Failed to fetch config:', error)
setError("Failed to fetch config")
}
}

fetchConfig()
}, [])

if (error !== null) {
return <div>{error}</div>
}

if (config === null) {
return <div>Loading...</div>
}

return (
<ConfigContext.Provider value={{ config }}>
{children}
Expand Down

0 comments on commit 0a8c565

Please sign in to comment.