Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 2.41.0

- The home page is now an operator dashboard with the nginx status, resource counts, expiring certificates, recent error
logs, and a traffic summary.
- The feature tour with help videos moved to a dedicated Help page, accessible from the question-mark icon in the
header menu.
- Development workflow improvements
- Fixed double scrollbar on the logs page after the UI overhaul

Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ RUN apk add --no-cache \
zlib \
openssl \
ca-certificates \
curl && \
curl \
procps && \
update-ca-certificates

ARG TARGETPLATFORM
Expand Down
7 changes: 5 additions & 2 deletions api/nginx/status_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ type statusHandler struct {
}

func (h statusHandler) handle(ctx *gin.Context) {
running := h.commands.GetStatus(ctx.Request.Context())
ctx.JSON(http.StatusOK, gin.H{"running": running})
status := h.commands.GetStatus(ctx.Request.Context())
ctx.JSON(http.StatusOK, gin.H{
"running": status.Running,
"uptimeSeconds": status.UptimeSeconds,
})
}
8 changes: 5 additions & 3 deletions api/nginx/status_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ func Test_statusHandler(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()

uptime := int64(3600)
commands := nginx.NewMockedCommands(controller)
commands.EXPECT().
GetStatus(gomock.Any()).
Return(true)
Return(nginx.Status{Running: true, UptimeSeconds: &uptime})

handler := statusHandler{
commands: commands,
Expand All @@ -39,9 +40,10 @@ func Test_statusHandler(t *testing.T) {
engine.ServeHTTP(recorder, request)

assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]bool
var response map[string]any
json.Unmarshal(recorder.Body.Bytes(), &response)
assert.True(t, response["running"])
assert.True(t, response["running"].(bool))
assert.Equal(t, float64(3600), response["uptimeSeconds"].(float64))
})
})
}
2 changes: 1 addition & 1 deletion core/nginx/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Commands interface {
search *LogSearch,
) ([]logline.LogLine, error)
GetMainLogs(ctx context.Context, lines int, search *LogSearch) ([]logline.LogLine, error)
GetStatus(ctx context.Context) bool
GetStatus(ctx context.Context) Status
GetTrafficStats(ctx context.Context) (*Stats, error)
GetConfigFiles(ctx context.Context, input GetConfigFilesInput) ([]byte, error)
GetMetadata(ctx context.Context) (*Metadata, error)
Expand Down
5 changes: 5 additions & 0 deletions core/nginx/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const (
NoneSupportType SupportType = "NONE"
)

type Status struct {
UptimeSeconds *int64
Running bool
}

type Stats struct {
ServerZones map[string]StatsZoneData
FilterZones map[string]map[string]StatsZoneData
Expand Down
10 changes: 10 additions & 0 deletions core/nginx/process_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package nginx
import (
"os"
"path/filepath"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -52,4 +53,13 @@ func Test_processManager(t *testing.T) {
assert.NoFileExists(t, socketFile)
})
})

t.Run("uptimeSeconds", func(t *testing.T) {
pidFile := filepath.Join(tmpDir, "nginx.pid")
_ = os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0o644)

uptime, err := manager.uptimeSeconds()
assert.NoError(t, err)
assert.GreaterOrEqual(t, uptime, int64(0))
})
}
41 changes: 41 additions & 0 deletions core/nginx/process_manager_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
package nginx

import (
"errors"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"

"dillmann.com.br/nginx-ignition/core/common/log"
)
Expand All @@ -27,3 +32,39 @@ func (m *processManager) start() error {
log.Infof("nginx started")
return nil
}

func (m *processManager) uptimeSeconds() (int64, error) {
pid, err := m.currentPid()
if err != nil {
return 0, err
}
if pid == 0 {
return 0, errors.New("nginx is not running")
}

return processUptimeSeconds(pid)
}

func processUptimeSeconds(pid int64) (int64, error) {
command := exec.Command("ps", "-o", "lstart=", "-p", strconv.FormatInt(pid, 10))
output, err := command.Output()
if err != nil {
return 0, err
}

startText := strings.TrimSpace(string(output))
if startText == "" {
return 0, errors.New("process not found")
}

startTime, err := time.ParseInLocation("Mon Jan _2 15:04:05 2006", startText, time.Local)
if err != nil {
return 0, err
}

seconds := int64(time.Since(startTime).Seconds())
if seconds < 0 {
return 0, nil
}
return seconds, nil
}
32 changes: 32 additions & 0 deletions core/nginx/process_manager_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"os"
"strings"
"syscall"
"time"

"dillmann.com.br/nginx-ignition/core/common/log"
Expand All @@ -27,6 +28,37 @@ func (m *processManager) start() error {
return nil
}

func (m *processManager) uptimeSeconds() (int64, error) {
pid, err := m.currentPid()
if err != nil {
return 0, err
}

if pid == 0 {
return 0, errors.New("nginx is not running")
}

handle, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, uint32(pid))
if err != nil {
return 0, err
}

defer syscall.CloseHandle(handle)
var creationTime, exitTime, kernelTime, userTime syscall.Filetime

if err := syscall.GetProcessTimes(handle, &creationTime, &exitTime, &kernelTime, &userTime); err != nil {
return 0, err
}

startTime := time.Unix(0, creationTime.Nanoseconds())
seconds := int64(time.Since(startTime).Seconds())
if seconds < 0 {
return 0, nil
}

return seconds, nil
}

func (m *processManager) runBackgroundCommand(waitDelay time.Duration, extraArgs ...string) error {
cmd := m.prepareCommand(extraArgs...)

Expand Down
17 changes: 15 additions & 2 deletions core/nginx/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,21 @@ func (s *service) Stop(ctx context.Context) error {
})
}

func (s *service) GetStatus(_ context.Context) bool {
return s.semaphore.currentState() == runningState
func (s *service) GetStatus(_ context.Context) Status {
running := s.semaphore.currentState() == runningState
status := Status{Running: running}
if !running {
return status
}

uptime, err := s.processManager.uptimeSeconds()
if err != nil {
log.Warnf("unable to resolve nginx uptime: %v", err)
return status
}

status.UptimeSeconds = &uptime
return status
}

func (s *service) GetHostLogs(
Expand Down
18 changes: 14 additions & 4 deletions core/nginx/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,22 +169,32 @@ func Test_service(t *testing.T) {
})

t.Run("GetStatus", func(t *testing.T) {
t.Run("returns true when running", func(t *testing.T) {
t.Run("returns running when running", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "nginx-status-test")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)

nginxService := &service{
semaphore: &semaphore{
state: runningState,
},
processManager: &processManager{
configPath: tmpDir,
},
}
assert.True(t, nginxService.GetStatus(t.Context()))
status := nginxService.GetStatus(t.Context())
assert.True(t, status.Running)
})

t.Run("returns false when stopped", func(t *testing.T) {
t.Run("returns not running when stopped", func(t *testing.T) {
nginxService := &service{
semaphore: &semaphore{
state: stoppedState,
},
}
assert.False(t, nginxService.GetStatus(t.Context()))
status := nginxService.GetStatus(t.Context())
assert.False(t, status.Running)
assert.Nil(t, status.UptimeSeconds)
})
})
}
7 changes: 7 additions & 0 deletions frontend/src/domain/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react"
import LoginPage from "./authentication/LoginPage"
import HomePage from "./home/HomePage"
import HelpPage from "./help/HelpPage"
import AppRoute from "../core/components/router/AppRoute"
import OnboardingPage from "./onboarding/OnboardingPage"
import {
Expand Down Expand Up @@ -237,6 +238,12 @@ const Routes: AppRoute[] = [
icon: <TeamOutlined />,
},
},
{
path: "/help",
requiresAuthentication: true,
fullPage: false,
component: <HelpPage />,
},
{
path: "/",
requiresAuthentication: true,
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/domain/certificate/CertificateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ export default class CertificateService {
return this.gateway.getPage(pageSize, pageNumber, searchTerms).then(requireSuccessPayload)
}

async listAll(searchTerms?: string): Promise<CertificateResponse[]> {
const pageSize = 100
const certificates: CertificateResponse[] = []
let pageNumber = 0

while (true) {
const page = await this.list(pageSize, pageNumber, searchTerms)
certificates.push(...page.contents)

if (page.contents.length === 0 || certificates.length >= page.totalItems) break

pageNumber++
}

return certificates
}

async delete(id: string): Promise<void> {
return this.gateway.delete(id).then(requireSuccessResponse)
}
Expand Down
83 changes: 83 additions & 0 deletions frontend/src/domain/help/HelpPage.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.help-guide-container h1 {
font-size: 38px;
margin: 0;
text-align: center;
}

.help-guide-subtitle {
margin: 10px 0 0 !important;
font-size: 28px !important;
text-align: center;
}

.help-guide-header-container {
background: var(--nginxIgnition-shellHeaderBg, var(--nginxIgnition-colorBgLayout));
padding: 120px 40px;
margin-bottom: 50px;
}

.help-guide-container h2 {
font-size: 28px;
margin-top: 20px;
}

.help-guide-container p {
font-size: 16px;
margin: 10px 0;
padding: 0;
}

.help-guide-right-side-video,
.help-guide-left-side-video {
flex-grow: 0;
flex-shrink: 0;
width: 40%;
}

.help-guide-container video {
width: 100%;
}

.help-guide-right-side-video {
margin: 0 0 0 40px;
}

.help-guide-left-side-video {
margin: 0 40px 0 0;
}

.help-guide-video-mask {
border-radius: 10px;
overflow: hidden;
height: fit-content;
line-height: 0;
}

.help-guide-video-mask video {
margin: 0;
padding: 0;
line-height: 0 !important;
}

.help-guide-section {
margin-bottom: 80px;
padding: 0 40px;
}

.help-guide-section-content {
flex-grow: 1;
width: 60%;
}

.help-guide-footer-container {
background: var(--nginxIgnition-shellHeaderBg, var(--nginxIgnition-colorBgLayout));
padding: 60px 40px;
}

.help-guide-footer-container h1 {
font-size: 24px;
}

.help-guide-footer-container p {
font-size: 18px !important;
}
Loading
Loading