Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
remove excel for csv
Signed-off-by: Luke Roy <luke.roy@ibm.com>
  • Loading branch information
Luke-Roy-IBM committed Apr 22, 2026
commit 3987b2ce626b557f920afa3fbc800a0468d59a24
3 changes: 1 addition & 2 deletions serverless-fleet-worker-registration/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ fleet-register
*_test.go

# Output files
*.xlsx
*.xls
*.csv

# Git
.git
Expand Down
4 changes: 2 additions & 2 deletions serverless-fleet-worker-registration/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ EXPOSE 8080

RUN mkdir /fleet-workers

# Set environment variable for the workbook path
ENV WORKBOOK_PATH=/fleet-workers/fleet-register.xlsx
# Set environment variable for the CSV path
ENV CSV_PATH=/fleet-workers/fleet-register.csv

# Run the application
CMD ["/app/fleet-register"]
18 changes: 9 additions & 9 deletions serverless-fleet-worker-registration/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Fleet Register

A lightweight Go HTTP server that monitors IBM Cloud Code Engine fleet workers by tracking their lifecycle events in an Excel file.
A lightweight Go HTTP server that monitors IBM Cloud Code Engine fleet workers by tracking their lifecycle events in a CSV file.

## Use Case

This application is deployed as a Code Engine application to monitor fleet workers. Fleet workers call the `/register` endpoint when they start and the `/deregister` endpoint before they shut down. The application maintains a persistent Excel file tracking all worker activity, including registration and completion timestamps.
This application is deployed as a Code Engine application to monitor fleet workers. Fleet workers call the `/register` endpoint when they start and the `/deregister` endpoint before they shut down. The application maintains a persistent CSV file tracking all worker activity, including registration and completion timestamps.

**Deployment Flow:**
1. Deploy this app as a Code Engine application with persistent storage
Expand All @@ -14,14 +14,14 @@ This application is deployed as a Code Engine application to monitor fleet worke
## Endpoints

- `POST /register`
Creates a row in `fleet-register.xlsx` with status `running` and records the registration timestamp.
Creates a row in `fleet-register.csv` with status `running` and records the registration timestamp.

- `POST /deregister`
Updates the matching row by `worker_name` and `worker_ip` to `completed` and records the completion timestamp.
If no row exists, it creates a new row with status `completed`.

- `GET /download`
Downloads the Excel file.
Downloads the CSV file.

## Request body

Expand Down Expand Up @@ -93,7 +93,7 @@ docker build -t fleet-register .
docker run -p 8080:8080 -v fleet-data:/fleet-workers fleet-register
```

The Excel file is saved to `/fleet-workers/fleet-register.xlsx` and persisted in the volume.
The CSV file is saved to `/fleet-workers/fleet-register.csv` and persisted in the volume.

## Examples

Expand All @@ -119,17 +119,17 @@ curl -X POST http://localhost:8080/deregister \
}'
```

Download the workbook:
Download the CSV file:

```bash
curl -O -J http://localhost:8080/download
```

## Excel file
## CSV file

The server automatically creates `fleet-register.xlsx` in the project root (or `/fleet-workers/` in Docker) if it does not exist.
The server automatically creates `fleet-register.csv` in the project root (or `/fleet-workers/` in Docker) if it does not exist.

The workbook contains one sheet named `Workers` with these columns:
The CSV file contains the following columns:

- `worker_name` - Name of the worker
- `worker_ip` - IP address of the worker
Expand Down
13 changes: 0 additions & 13 deletions serverless-fleet-worker-registration/go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
module fleet-register

go 1.22

require github.com/xuri/excelize/v2 v2.8.1

require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
177 changes: 70 additions & 107 deletions serverless-fleet-worker-registration/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/csv"
"encoding/json"
"errors"
"fmt"
Expand All @@ -10,25 +11,22 @@ import (
"strings"
"sync"
"time"

"github.com/xuri/excelize/v2"
)

const (
serverAddr = ":8080"
sheetName = "Workers"
)

var workbookPath = getWorkbookPath()
var csvPath = getCSVPath()

func getWorkbookPath() string {
if path := os.Getenv("WORKBOOK_PATH"); path != "" {
func getCSVPath() string {
if path := os.Getenv("CSV_PATH"); path != "" {
return path
}
return "fleet-register.xlsx"
return "fleet-register.csv"
}

var workbookMu sync.Mutex
var csvMu sync.Mutex

type workerRequest struct {
WorkerName string `json:"worker_name"`
Expand Down Expand Up @@ -64,8 +62,8 @@ func registerHandler(w http.ResponseWriter, r *http.Request) {
return
}

workbookMu.Lock()
defer workbookMu.Unlock()
csvMu.Lock()
defer csvMu.Unlock()

if err := appendWorkerRow(req, "running"); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to register worker: %v", err))
Expand All @@ -77,7 +75,7 @@ func registerHandler(w http.ResponseWriter, r *http.Request) {
WorkerName: req.WorkerName,
WorkerIP: req.WorkerIP,
Status: "running",
File: workbookPath,
File: csvPath,
})
}

Expand All @@ -88,8 +86,8 @@ func deregisterHandler(w http.ResponseWriter, r *http.Request) {
return
}

workbookMu.Lock()
defer workbookMu.Unlock()
csvMu.Lock()
defer csvMu.Unlock()

updated, err := completeWorker(req)
if err != nil {
Expand All @@ -107,27 +105,27 @@ func deregisterHandler(w http.ResponseWriter, r *http.Request) {
WorkerName: req.WorkerName,
WorkerIP: req.WorkerIP,
Status: "completed",
File: workbookPath,
File: csvPath,
})
}

func downloadHandler(w http.ResponseWriter, r *http.Request) {
workbookMu.Lock()
defer workbookMu.Unlock()
csvMu.Lock()
defer csvMu.Unlock()

if err := ensureWorkbook(); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to prepare workbook: %v", err))
if err := ensureCSV(); err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to prepare CSV: %v", err))
return
}

content, err := os.ReadFile(workbookPath)
content, err := os.ReadFile(csvPath)
if err != nil {
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to read workbook: %v", err))
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to read CSV: %v", err))
return
}

w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, workbookPath))
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, csvPath))
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content)))
w.WriteHeader(http.StatusOK)

Expand Down Expand Up @@ -159,133 +157,98 @@ func decodeWorkerRequest(r *http.Request) (workerRequest, error) {
}

func appendWorkerRow(req workerRequest, status string) error {
file, err := openWorkbook()
if err != nil {
if err := ensureCSV(); err != nil {
return err
}
defer closeWorkbook(file)

rows, err := file.GetRows(sheetName)
file, err := os.OpenFile(csvPath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()

nextRow := len(rows) + 1
registeredAt := time.Now().Format(time.RFC3339)
values := []interface{}{req.WorkerName, req.WorkerIP, status, registeredAt, ""}

cell, err := excelize.CoordinatesToCellName(1, nextRow)
if err != nil {
return err
}
writer := csv.NewWriter(file)
defer writer.Flush()

if err := file.SetSheetRow(sheetName, cell, &values); err != nil {
return err
}
registeredAt := time.Now().Format(time.RFC3339)
record := []string{req.WorkerName, req.WorkerIP, status, registeredAt, ""}

return file.SaveAs(workbookPath)
return writer.Write(record)
}

func completeWorker(req workerRequest) (bool, error) {
file, err := openWorkbook()
if err := ensureCSV(); err != nil {
return false, err
}

// Read all records
file, err := os.Open(csvPath)
if err != nil {
return false, err
}
defer closeWorkbook(file)

rows, err := file.GetRows(sheetName)
reader := csv.NewReader(file)
records, err := reader.ReadAll()
file.Close()
if err != nil {
return false, err
}

completedAt := time.Now().Format(time.RFC3339)

for index := 1; index < len(rows); index++ {
row := rows[index]
if len(row) < 2 {
continue
}

if row[0] == req.WorkerName && row[1] == req.WorkerIP {
// Update status to completed (column 3)
statusCell, err := excelize.CoordinatesToCellName(3, index+1)
if err != nil {
return false, err
}

if err := file.SetCellValue(sheetName, statusCell, "completed"); err != nil {
return false, err
}

// Update completed_at timestamp (column 5)
completedCell, err := excelize.CoordinatesToCellName(5, index+1)
if err != nil {
return false, err
}

if err := file.SetCellValue(sheetName, completedCell, completedAt); err != nil {
return false, err
}

return true, file.SaveAs(workbookPath)
updated := false

// Update matching record
for i := 1; i < len(records); i++ {
if len(records[i]) >= 2 && records[i][0] == req.WorkerName && records[i][1] == req.WorkerIP {
records[i][2] = "completed"
records[i][4] = completedAt
updated = true
break
}
}

// If worker not found, add new row with completed status
values := []interface{}{req.WorkerName, req.WorkerIP, "completed", "", completedAt}
nextRow := len(rows) + 1
// If not found, add new record
if !updated {
records = append(records, []string{req.WorkerName, req.WorkerIP, "completed", "", completedAt})
}

cell, err := excelize.CoordinatesToCellName(1, nextRow)
// Write all records back
file, err = os.Create(csvPath)
if err != nil {
return false, err
}
defer file.Close()

if err := file.SetSheetRow(sheetName, cell, &values); err != nil {
return false, err
writer := csv.NewWriter(file)
defer writer.Flush()

for _, record := range records {
if err := writer.Write(record); err != nil {
return false, err
}
}

return false, file.SaveAs(workbookPath)
return updated, nil
}

func ensureWorkbook() error {
if _, err := os.Stat(workbookPath); err == nil {
func ensureCSV() error {
if _, err := os.Stat(csvPath); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}

file := excelize.NewFile()
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())

if defaultSheet != sheetName {
file.SetSheetName(defaultSheet, sheetName)
}

headers := []interface{}{"worker_name", "worker_ip", "status", "registered_at", "completed_at"}
if err := file.SetSheetRow(sheetName, "A1", &headers); err != nil {
return err
}

return file.SaveAs(workbookPath)
}

func openWorkbook() (*excelize.File, error) {
if err := ensureWorkbook(); err != nil {
return nil, err
}

file, err := excelize.OpenFile(workbookPath)
file, err := os.Create(csvPath)
if err != nil {
return nil, err
return err
}
defer file.Close()

return file, nil
}
writer := csv.NewWriter(file)
defer writer.Flush()

func closeWorkbook(file *excelize.File) {
if err := file.Close(); err != nil {
log.Printf("failed to close workbook: %v", err)
}
headers := []string{"worker_name", "worker_ip", "status", "registered_at", "completed_at"}
return writer.Write(headers)
}

func writeJSON(w http.ResponseWriter, statusCode int, payload apiResponse) {
Expand Down
Loading