Skip to content

Commit 3987b2c

Browse files
committed
remove excel for csv
Signed-off-by: Luke Roy <luke.roy@ibm.com>
1 parent c742ddd commit 3987b2c

5 files changed

Lines changed: 82 additions & 133 deletions

File tree

serverless-fleet-worker-registration/.dockerignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ fleet-register
1010
*_test.go
1111

1212
# Output files
13-
*.xlsx
14-
*.xls
13+
*.csv
1514

1615
# Git
1716
.git

serverless-fleet-worker-registration/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ EXPOSE 8080
3232

3333
RUN mkdir /fleet-workers
3434

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

3838
# Run the application
3939
CMD ["/app/fleet-register"]

serverless-fleet-worker-registration/README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Fleet Register
22

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

55
## Use Case
66

7-
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.
7+
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.
88

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

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

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

2323
- `GET /download`
24-
Downloads the Excel file.
24+
Downloads the CSV file.
2525

2626
## Request body
2727

@@ -93,7 +93,7 @@ docker build -t fleet-register .
9393
docker run -p 8080:8080 -v fleet-data:/fleet-workers fleet-register
9494
```
9595

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

9898
## Examples
9999

@@ -119,17 +119,17 @@ curl -X POST http://localhost:8080/deregister \
119119
}'
120120
```
121121

122-
Download the workbook:
122+
Download the CSV file:
123123

124124
```bash
125125
curl -O -J http://localhost:8080/download
126126
```
127127

128-
## Excel file
128+
## CSV file
129129

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

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

134134
- `worker_name` - Name of the worker
135135
- `worker_ip` - IP address of the worker
Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
11
module fleet-register
22

33
go 1.22
4-
5-
require github.com/xuri/excelize/v2 v2.8.1
6-
7-
require (
8-
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
9-
github.com/richardlehane/mscfb v1.0.4 // indirect
10-
github.com/richardlehane/msoleps v1.0.3 // indirect
11-
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
12-
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
13-
golang.org/x/crypto v0.19.0 // indirect
14-
golang.org/x/net v0.21.0 // indirect
15-
golang.org/x/text v0.14.0 // indirect
16-
)

serverless-fleet-worker-registration/main.go

Lines changed: 70 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"encoding/csv"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -10,25 +11,22 @@ import (
1011
"strings"
1112
"sync"
1213
"time"
13-
14-
"github.com/xuri/excelize/v2"
1514
)
1615

1716
const (
1817
serverAddr = ":8080"
19-
sheetName = "Workers"
2018
)
2119

22-
var workbookPath = getWorkbookPath()
20+
var csvPath = getCSVPath()
2321

24-
func getWorkbookPath() string {
25-
if path := os.Getenv("WORKBOOK_PATH"); path != "" {
22+
func getCSVPath() string {
23+
if path := os.Getenv("CSV_PATH"); path != "" {
2624
return path
2725
}
28-
return "fleet-register.xlsx"
26+
return "fleet-register.csv"
2927
}
3028

31-
var workbookMu sync.Mutex
29+
var csvMu sync.Mutex
3230

3331
type workerRequest struct {
3432
WorkerName string `json:"worker_name"`
@@ -64,8 +62,8 @@ func registerHandler(w http.ResponseWriter, r *http.Request) {
6462
return
6563
}
6664

67-
workbookMu.Lock()
68-
defer workbookMu.Unlock()
65+
csvMu.Lock()
66+
defer csvMu.Unlock()
6967

7068
if err := appendWorkerRow(req, "running"); err != nil {
7169
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to register worker: %v", err))
@@ -77,7 +75,7 @@ func registerHandler(w http.ResponseWriter, r *http.Request) {
7775
WorkerName: req.WorkerName,
7876
WorkerIP: req.WorkerIP,
7977
Status: "running",
80-
File: workbookPath,
78+
File: csvPath,
8179
})
8280
}
8381

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

91-
workbookMu.Lock()
92-
defer workbookMu.Unlock()
89+
csvMu.Lock()
90+
defer csvMu.Unlock()
9391

9492
updated, err := completeWorker(req)
9593
if err != nil {
@@ -107,27 +105,27 @@ func deregisterHandler(w http.ResponseWriter, r *http.Request) {
107105
WorkerName: req.WorkerName,
108106
WorkerIP: req.WorkerIP,
109107
Status: "completed",
110-
File: workbookPath,
108+
File: csvPath,
111109
})
112110
}
113111

114112
func downloadHandler(w http.ResponseWriter, r *http.Request) {
115-
workbookMu.Lock()
116-
defer workbookMu.Unlock()
113+
csvMu.Lock()
114+
defer csvMu.Unlock()
117115

118-
if err := ensureWorkbook(); err != nil {
119-
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to prepare workbook: %v", err))
116+
if err := ensureCSV(); err != nil {
117+
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to prepare CSV: %v", err))
120118
return
121119
}
122120

123-
content, err := os.ReadFile(workbookPath)
121+
content, err := os.ReadFile(csvPath)
124122
if err != nil {
125-
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to read workbook: %v", err))
123+
writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to read CSV: %v", err))
126124
return
127125
}
128126

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

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

161159
func appendWorkerRow(req workerRequest, status string) error {
162-
file, err := openWorkbook()
163-
if err != nil {
160+
if err := ensureCSV(); err != nil {
164161
return err
165162
}
166-
defer closeWorkbook(file)
167163

168-
rows, err := file.GetRows(sheetName)
164+
file, err := os.OpenFile(csvPath, os.O_APPEND|os.O_WRONLY, 0644)
169165
if err != nil {
170166
return err
171167
}
168+
defer file.Close()
172169

173-
nextRow := len(rows) + 1
174-
registeredAt := time.Now().Format(time.RFC3339)
175-
values := []interface{}{req.WorkerName, req.WorkerIP, status, registeredAt, ""}
176-
177-
cell, err := excelize.CoordinatesToCellName(1, nextRow)
178-
if err != nil {
179-
return err
180-
}
170+
writer := csv.NewWriter(file)
171+
defer writer.Flush()
181172

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

186-
return file.SaveAs(workbookPath)
176+
return writer.Write(record)
187177
}
188178

189179
func completeWorker(req workerRequest) (bool, error) {
190-
file, err := openWorkbook()
180+
if err := ensureCSV(); err != nil {
181+
return false, err
182+
}
183+
184+
// Read all records
185+
file, err := os.Open(csvPath)
191186
if err != nil {
192187
return false, err
193188
}
194-
defer closeWorkbook(file)
195189

196-
rows, err := file.GetRows(sheetName)
190+
reader := csv.NewReader(file)
191+
records, err := reader.ReadAll()
192+
file.Close()
197193
if err != nil {
198194
return false, err
199195
}
200196

201197
completedAt := time.Now().Format(time.RFC3339)
202-
203-
for index := 1; index < len(rows); index++ {
204-
row := rows[index]
205-
if len(row) < 2 {
206-
continue
207-
}
208-
209-
if row[0] == req.WorkerName && row[1] == req.WorkerIP {
210-
// Update status to completed (column 3)
211-
statusCell, err := excelize.CoordinatesToCellName(3, index+1)
212-
if err != nil {
213-
return false, err
214-
}
215-
216-
if err := file.SetCellValue(sheetName, statusCell, "completed"); err != nil {
217-
return false, err
218-
}
219-
220-
// Update completed_at timestamp (column 5)
221-
completedCell, err := excelize.CoordinatesToCellName(5, index+1)
222-
if err != nil {
223-
return false, err
224-
}
225-
226-
if err := file.SetCellValue(sheetName, completedCell, completedAt); err != nil {
227-
return false, err
228-
}
229-
230-
return true, file.SaveAs(workbookPath)
198+
updated := false
199+
200+
// Update matching record
201+
for i := 1; i < len(records); i++ {
202+
if len(records[i]) >= 2 && records[i][0] == req.WorkerName && records[i][1] == req.WorkerIP {
203+
records[i][2] = "completed"
204+
records[i][4] = completedAt
205+
updated = true
206+
break
231207
}
232208
}
233209

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

238-
cell, err := excelize.CoordinatesToCellName(1, nextRow)
215+
// Write all records back
216+
file, err = os.Create(csvPath)
239217
if err != nil {
240218
return false, err
241219
}
220+
defer file.Close()
242221

243-
if err := file.SetSheetRow(sheetName, cell, &values); err != nil {
244-
return false, err
222+
writer := csv.NewWriter(file)
223+
defer writer.Flush()
224+
225+
for _, record := range records {
226+
if err := writer.Write(record); err != nil {
227+
return false, err
228+
}
245229
}
246230

247-
return false, file.SaveAs(workbookPath)
231+
return updated, nil
248232
}
249233

250-
func ensureWorkbook() error {
251-
if _, err := os.Stat(workbookPath); err == nil {
234+
func ensureCSV() error {
235+
if _, err := os.Stat(csvPath); err == nil {
252236
return nil
253237
} else if !os.IsNotExist(err) {
254238
return err
255239
}
256240

257-
file := excelize.NewFile()
258-
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
259-
260-
if defaultSheet != sheetName {
261-
file.SetSheetName(defaultSheet, sheetName)
262-
}
263-
264-
headers := []interface{}{"worker_name", "worker_ip", "status", "registered_at", "completed_at"}
265-
if err := file.SetSheetRow(sheetName, "A1", &headers); err != nil {
266-
return err
267-
}
268-
269-
return file.SaveAs(workbookPath)
270-
}
271-
272-
func openWorkbook() (*excelize.File, error) {
273-
if err := ensureWorkbook(); err != nil {
274-
return nil, err
275-
}
276-
277-
file, err := excelize.OpenFile(workbookPath)
241+
file, err := os.Create(csvPath)
278242
if err != nil {
279-
return nil, err
243+
return err
280244
}
245+
defer file.Close()
281246

282-
return file, nil
283-
}
247+
writer := csv.NewWriter(file)
248+
defer writer.Flush()
284249

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

291254
func writeJSON(w http.ResponseWriter, statusCode int, payload apiResponse) {

0 commit comments

Comments
 (0)