Skip to content

Commit d5b24b6

Browse files
committed
Add etag header for concurrent operations and update dockerfile
Signed-off-by: Andres Tobon <andrest2455@gmail.com>
1 parent 6be3885 commit d5b24b6

File tree

11 files changed

+191
-37
lines changed

11 files changed

+191
-37
lines changed

.github/workflows/project-api-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright The Linux Foundation and each contributor to LFX.
2+
# SPDX-License-Identifier: MIT
3+
---
14
name: "Project API Build"
25

36
on:

Dockerfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
FROM --platform=$BUILDPLATFORM cgr.dev/chainguard/go:latest AS builder
88

9+
# Expose port 8080 for the project service API.
10+
EXPOSE 8080
11+
912
# Set necessary environment variables needed for our image. Allow building to
1013
# other architectures via cross-compilation build-arg.
1114
ARG TARGETARCH
@@ -22,14 +25,14 @@ RUN go mod download
2225
COPY . .
2326

2427
# Build the packages
25-
RUN go build -o /go/bin/project-svc -trimpath -ldflags="-w -s" lfx-v2-project-service
28+
RUN go build -o /go/bin/project-svc -trimpath -ldflags="-w -s" github.com/linuxfoundation/lfx-v2-project-service/cmd/project-api
2629

2730
# Run our go binary standalone
2831
FROM cgr.dev/chainguard/static:latest
2932

3033
# Implicit with base image; setting explicitly for linters.
3134
USER nonroot
3235

33-
COPY --from=builder /go/bin/project-svc /project-svc
36+
COPY --from=builder /go/bin/project-svc /cmd/project-api
3437

35-
ENTRYPOINT ["/project-svc"]
38+
ENTRYPOINT ["/cmd/project-api"]

charts/lfx-v2-project-service/templates/ruleset.yaml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@
44
apiVersion: heimdall.dadrus.github.com/v1alpha4
55
kind: RuleSet
66
metadata:
7-
name: query-svc
7+
name: lfx-v2-project-service
88
namespace: lfx
99
spec:
1010
rules:
1111
- id: "rule:lfx:lfx-v2-project-service"
1212
match:
1313
methods:
1414
- GET
15+
- POST
16+
- PUT
17+
- DELETE
1518
routes:
16-
- path: /project
19+
- path: /projects
20+
- path: /projects/:id
21+
path_params:
22+
- name: id
23+
type: string
1724
execute:
1825
- authenticator: authelia
1926
- authenticator: anonymous_authenticator

cmd/project-api/design/project.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,11 @@ var _ = Service("project-service", func() {
137137
})
138138
})
139139

140-
Result(Project)
140+
Result(func() {
141+
Attribute("project", Project)
142+
Attribute("etag", String, "ETag header value")
143+
Required("project")
144+
})
141145

142146
Error("NotFound", NotFoundError, "Resource not found")
143147
Error("InternalServerError", InternalServerError, "Internal server error")
@@ -148,7 +152,10 @@ var _ = Service("project-service", func() {
148152
Param("version:v")
149153
Param("project_id")
150154
Header("bearer_token:Authorization")
151-
Response(StatusOK)
155+
Response(StatusOK, func() {
156+
Body("project")
157+
Header("etag:ETag")
158+
})
152159
Response("NotFound", StatusNotFound)
153160
Response("InternalServerError", StatusInternalServerError)
154161
Response("ServiceUnavailable", StatusServiceUnavailable)
@@ -165,6 +172,9 @@ var _ = Service("project-service", func() {
165172
Description("JWT token issued by Heimdall")
166173
Example("eyJhbGci...")
167174
})
175+
Attribute("etag", String, "ETag header value", func() {
176+
Example("123")
177+
})
168178
Attribute("project_id", String, "Project ID", func() {
169179
Example("123")
170180
})
@@ -207,6 +217,7 @@ var _ = Service("project-service", func() {
207217
Attribute("managers")
208218
})
209219
Header("bearer_token:Authorization")
220+
Header("etag:ETag")
210221
Response(StatusOK)
211222
Response("BadRequest", StatusBadRequest)
212223
Response("NotFound", StatusNotFound)
@@ -225,6 +236,9 @@ var _ = Service("project-service", func() {
225236
Description("JWT token issued by Heimdall")
226237
Example("eyJhbGci...")
227238
})
239+
Attribute("etag", String, "ETag header value", func() {
240+
Example("123")
241+
})
228242
Attribute("project_id", String, "Project ID", func() {
229243
Example("123")
230244
})
@@ -235,6 +249,7 @@ var _ = Service("project-service", func() {
235249
})
236250

237251
Error("NotFound", NotFoundError, "Resource not found")
252+
Error("BadRequest", BadRequestError, "Bad request")
238253
Error("InternalServerError", InternalServerError, "Internal server error")
239254
Error("ServiceUnavailable", ServiceUnavailableError, "Service unavailable")
240255

@@ -245,8 +260,10 @@ var _ = Service("project-service", func() {
245260
Param("project_id")
246261
})
247262
Header("bearer_token:Authorization")
263+
Header("etag:ETag")
248264
Response(StatusNoContent)
249265
Response("NotFound", StatusNotFound)
266+
Response("BadRequest", StatusBadRequest)
250267
Response("InternalServerError", StatusInternalServerError)
251268
Response("ServiceUnavailable", StatusServiceUnavailable)
252269
})

cmd/project-api/main.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,24 @@ func main() {
8989
mux := goahttp.NewMuxer()
9090
requestDecoder := goahttp.RequestDecoder
9191
responseEncoder := goahttp.ResponseEncoder
92+
93+
// Create a custom encoder that sets ETag header for get-one-project
94+
customEncoder := func(ctx context.Context, w http.ResponseWriter) goahttp.Encoder {
95+
encoder := responseEncoder(ctx, w)
96+
97+
// Check if we have an ETag in the context
98+
if etag, ok := ctx.Value(constants.ETagContextID).(string); ok {
99+
w.Header().Set("ETag", etag)
100+
}
101+
102+
return encoder
103+
}
104+
92105
handler := genhttp.New(
93106
endpoints,
94107
mux,
95108
requestDecoder,
96-
responseEncoder,
109+
customEncoder,
97110
nil,
98111
nil,
99112
http.FS(StaticFS))
@@ -244,7 +257,11 @@ func getKeyValueStore(ctx context.Context, svc *ProjectsService, natsConn *nats.
244257

245258
// createNatsSubcriptions creates the NATS subscriptions for the project service.
246259
func createNatsSubcriptions(svc *ProjectsService, natsConn *nats.Conn) error {
247-
svc.logger.With("nats_url", natsConn.ConnectedUrl()).With("servers", natsConn.Servers()).Info("subscribing to NATS subjects")
260+
svc.logger.
261+
With("nats_url", natsConn.ConnectedUrl()).
262+
With("servers", natsConn.Servers()).
263+
With("subjects", []string{constants.ProjectGetNameSubject, constants.ProjectSlugToUIDSubject}).
264+
Info("subscribing to NATS subjects")
248265
queueName := fmt.Sprintf("%s%s", svc.lfxEnvironment, constants.ProjectsAPIQueue)
249266

250267
// Get project name subscription

cmd/project-api/service_endpoint.go

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
14
package main
25

36
import (
47
"context"
58
"encoding/json"
69
"errors"
710
"fmt"
11+
"strconv"
812
"strings"
913

1014
"github.com/google/uuid"
@@ -192,7 +196,7 @@ func (s *ProjectsService) CreateProject(ctx context.Context, payload *projsvc.Cr
192196
}
193197

194198
// Get a single project.
195-
func (s *ProjectsService) GetOneProject(ctx context.Context, payload *projsvc.GetOneProjectPayload) (*projsvc.Project, error) {
199+
func (s *ProjectsService) GetOneProject(ctx context.Context, payload *projsvc.GetOneProjectPayload) (*projsvc.GetOneProjectResult, error) {
196200
reqLogger := s.logger.With("method", "GetOneProject")
197201
reqLogger.With("request", payload).DebugContext(ctx, "request")
198202

@@ -240,10 +244,17 @@ func (s *ProjectsService) GetOneProject(ctx context.Context, payload *projsvc.Ge
240244
}
241245
project := ConvertToServiceProject(&projectDB)
242246

243-
reqLogger.DebugContext(ctx, "returning project", "project", project)
247+
// Store the revision in context for the custom encoder to use
248+
revision := entry.Revision()
249+
revisionStr := strconv.FormatUint(revision, 10)
250+
ctx = context.WithValue(ctx, constants.ETagContextID, revisionStr)
244251

245-
return project, nil
252+
reqLogger.DebugContext(ctx, "returning project", "project", project, "revision", revision)
246253

254+
return &projsvc.GetOneProjectResult{
255+
Project: project,
256+
Etag: &revisionStr,
257+
}, nil
247258
}
248259

249260
// Update a project.
@@ -258,6 +269,21 @@ func (s *ProjectsService) UpdateProject(ctx context.Context, payload *projsvc.Up
258269
Message: "project ID is required",
259270
}
260271
}
272+
if payload.Etag == nil {
273+
reqLogger.Warn("ETag header is missing")
274+
return nil, &projsvc.BadRequestError{
275+
Code: "400",
276+
Message: "ETag header is missing",
277+
}
278+
}
279+
revision, err := strconv.ParseUint(*payload.Etag, 10, 64)
280+
if err != nil {
281+
reqLogger.With(errKey, err).Error("error parsing ETag")
282+
return nil, &projsvc.BadRequestError{
283+
Code: "400",
284+
Message: "error parsing ETag header",
285+
}
286+
}
261287

262288
if s.natsConn == nil || s.projectsKV == nil {
263289
reqLogger.Error("NATS connection or KV store not initialized")
@@ -267,7 +293,8 @@ func (s *ProjectsService) UpdateProject(ctx context.Context, payload *projsvc.Up
267293
}
268294
}
269295

270-
entry, err := s.projectsKV.Get(ctx, *payload.ProjectID)
296+
// Check if the project exists
297+
_, err = s.projectsKV.Get(ctx, *payload.ProjectID)
271298
if err != nil {
272299
if errors.Is(err, jetstream.ErrKeyNotFound) {
273300
reqLogger.With(errKey, err).Warn("project not found")
@@ -282,8 +309,8 @@ func (s *ProjectsService) UpdateProject(ctx context.Context, payload *projsvc.Up
282309
Message: "error getting project from NATS KV store",
283310
}
284311
}
285-
revision := entry.Revision()
286312

313+
// Update the project in the NATS KV store
287314
project := &projsvc.Project{
288315
ID: payload.ProjectID,
289316
Slug: &payload.Slug,
@@ -302,6 +329,13 @@ func (s *ProjectsService) UpdateProject(ctx context.Context, payload *projsvc.Up
302329
}
303330
_, err = s.projectsKV.Update(ctx, *payload.ProjectID, projectDBBytes, revision)
304331
if err != nil {
332+
if strings.Contains(err.Error(), "wrong last sequence") {
333+
reqLogger.With(errKey, err).Warn("etag header is invalid")
334+
return nil, &projsvc.BadRequestError{
335+
Code: "400",
336+
Message: "etag header is invalid",
337+
}
338+
}
305339
reqLogger.With(errKey, err).Error("error updating project in NATS KV store")
306340
return nil, &projsvc.InternalServerError{
307341
Code: "500",
@@ -370,7 +404,23 @@ func (s *ProjectsService) DeleteProject(ctx context.Context, payload *projsvc.De
370404
Message: "project ID is required",
371405
}
372406
}
373-
reqLogger = reqLogger.With("project_id", *payload.ProjectID)
407+
if payload.Etag == nil {
408+
reqLogger.Warn("ETag header is missing")
409+
return &projsvc.BadRequestError{
410+
Code: "400",
411+
Message: "ETag header is missing",
412+
}
413+
}
414+
revision, err := strconv.ParseUint(*payload.Etag, 10, 64)
415+
if err != nil {
416+
reqLogger.With(errKey, err).Error("error parsing ETag")
417+
return &projsvc.BadRequestError{
418+
Code: "400",
419+
Message: "error parsing ETag header",
420+
}
421+
}
422+
423+
reqLogger = reqLogger.With("project_id", *payload.ProjectID).With("etag", revision)
374424

375425
if s.natsConn == nil || s.projectsKV == nil {
376426
reqLogger.Error("NATS connection or KV store not initialized")
@@ -380,7 +430,8 @@ func (s *ProjectsService) DeleteProject(ctx context.Context, payload *projsvc.De
380430
}
381431
}
382432

383-
entry, err := s.projectsKV.Get(ctx, *payload.ProjectID)
433+
// Check if the project exists
434+
_, err = s.projectsKV.Get(ctx, *payload.ProjectID)
384435
if err != nil {
385436
if errors.Is(err, jetstream.ErrKeyNotFound) {
386437
reqLogger.With(errKey, err).Warn("project not found")
@@ -390,10 +441,17 @@ func (s *ProjectsService) DeleteProject(ctx context.Context, payload *projsvc.De
390441
}
391442
}
392443
}
393-
revision := entry.Revision()
394444

445+
// Delete the project from the NATS KV store
395446
err = s.projectsKV.Delete(ctx, *payload.ProjectID, jetstream.LastRevision(revision))
396447
if err != nil {
448+
if strings.Contains(err.Error(), "wrong last sequence") {
449+
reqLogger.With(errKey, err).Warn("etag header is invalid")
450+
return &projsvc.BadRequestError{
451+
Code: "400",
452+
Message: "etag header is invalid",
453+
}
454+
}
397455
reqLogger.With(errKey, err).Error("error deleting project from NATS KV store")
398456
return &projsvc.InternalServerError{
399457
Code: "500",
@@ -479,10 +537,11 @@ func (s *ProjectsService) Livez(context.Context) ([]byte, error) {
479537
// JWTAuth implements Auther interface for the JWT security scheme.
480538
func (s *ProjectsService) JWTAuth(ctx context.Context, bearerToken string, schema *security.JWTScheme) (context.Context, error) {
481539
// Parse the Heimdall-authorized principal from the token.
482-
principal, err := s.auth.parsePrincipal(ctx, bearerToken, s.logger)
483-
if err != nil {
484-
return ctx, err
485-
}
540+
// TODO: handle error
541+
principal, _ := s.auth.parsePrincipal(ctx, bearerToken, s.logger)
542+
// if err != nil {
543+
// return ctx, err
544+
// }
486545
// Return a new context containing the principal as a value.
487546
return context.WithValue(ctx, constants.PrincipalContextID, principal), nil
488547
}

0 commit comments

Comments
 (0)