Skip to content

Commit d2c417d

Browse files
authored
feat: add label endpoint (#369)
* chore: remove version from docker compose file Signed-off-by: Olivier Vernin <olivier@vernin.me> * feat: add label endpoint Signed-off-by: Olivier Vernin <olivier@vernin.me> * chore: add label post test case Signed-off-by: Olivier Vernin <olivier@vernin.me> * fix: use correctly variable Signed-off-by: Olivier Vernin <olivier@vernin.me> * feat: allow to filter pipelinereport and scm summary by label Signed-off-by: Olivier Vernin <olivier@vernin.me> * fix: test Signed-off-by: Olivier Vernin <olivier@vernin.me> * chore: various tidy up Signed-off-by: Olivier Vernin <olivier@vernin.me> * chore: various improvements * Use variable of type uuid over string * Fix swagger doc * Check againt pgx error instead of sql Signed-off-by: Olivier Vernin <olivier@vernin.me> * fix: typo Signed-off-by: Olivier Vernin <olivier@vernin.me> * chore: simplify code Signed-off-by: Olivier Vernin <olivier@vernin.me> * chore: make keyonly param more robust Signed-off-by: Olivier Vernin <olivier@vernin.me> --------- Signed-off-by: Olivier Vernin <olivier@vernin.me>
1 parent f22a228 commit d2c417d

19 files changed

Lines changed: 1623 additions & 658 deletions

docker-compose.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
version: '3'
21
services:
32
db:
43
image: postgres:17@sha256:8d3be35b184e70d81e54cbcbd3df3c0b47f37d06482c0dd1c140db5dbcc6a808

go.mod

Lines changed: 141 additions & 133 deletions
Large diffs are not rendered by default.

go.sum

Lines changed: 355 additions & 347 deletions
Large diffs are not rendered by default.

pkg/database/label.go

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
package database
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/google/uuid"
8+
"github.com/sirupsen/logrus"
9+
"github.com/updatecli/udash/pkg/model"
10+
11+
"github.com/stephenafamo/bob/dialect/psql"
12+
"github.com/stephenafamo/bob/dialect/psql/im"
13+
"github.com/stephenafamo/bob/dialect/psql/sm"
14+
)
15+
16+
// InsertLabel creates a new label and inserts it into the database.
17+
//
18+
// It returns the ID of the newly created label.
19+
func InsertLabel(ctx context.Context, key, value string) (string, error) {
20+
query := psql.Insert(
21+
im.Into("labels", "key", "value"),
22+
im.Values(psql.Arg(key), psql.Arg(value)),
23+
im.Returning("id"),
24+
)
25+
26+
queryString, args, err := query.Build(ctx)
27+
28+
if err != nil {
29+
logrus.Errorf("building query failed: %s\n\t%s", queryString, err)
30+
return "", err
31+
}
32+
33+
var id uuid.UUID
34+
err = DB.QueryRow(ctx, queryString, args...).Scan(
35+
&id,
36+
)
37+
38+
if err != nil {
39+
logrus.Errorf("query failed: %q\n\t%s", queryString, err)
40+
return "", err
41+
}
42+
43+
return id.String(), nil
44+
}
45+
46+
// GetLabelKeyOnlyRecords returns a list of labels from the labels database table.
47+
func GetLabelKeyOnlyRecords(ctx context.Context, startTime, endTime string, limit, page int) ([]string, int, error) {
48+
49+
query := psql.Select(
50+
sm.Columns("id", "key", "created_at", "updated_at", "last_pipeline_report_at"),
51+
sm.From("labels"),
52+
sm.OrderBy("key"),
53+
sm.Distinct("key"),
54+
)
55+
56+
if err := applyRangeFilter(
57+
"last_pipeline_report_at",
58+
dateRangeFilterParams{
59+
Query: &query,
60+
DateRangeDays: 0,
61+
StartTime: startTime,
62+
EndTime: endTime,
63+
}); err != nil {
64+
return nil, 0, fmt.Errorf("applying last_pipeline_report_at range filter: %w", err)
65+
}
66+
67+
totalCount := 0
68+
totalQuery := psql.Select(sm.From(query), sm.Columns("count(*)"))
69+
totalQueryString, totalArgs, err := totalQuery.Build(ctx)
70+
if err != nil {
71+
logrus.Errorf("building total count query failed: %s\n\t%s", totalQueryString, err)
72+
return nil, 0, err
73+
}
74+
75+
if err = DB.QueryRow(ctx, totalQueryString, totalArgs...).Scan(
76+
&totalCount,
77+
); err != nil {
78+
logrus.Errorf("parsing total count result: %s", err)
79+
}
80+
81+
if limit < totalCount && limit > 0 {
82+
query.Apply(
83+
sm.Limit(limit),
84+
sm.Offset((page-1)*limit),
85+
)
86+
}
87+
88+
queryString, args, err := query.Build(ctx)
89+
90+
if err != nil {
91+
logrus.Errorf("building query failed: %s\n\t%s", queryString, err)
92+
return nil, 0, err
93+
}
94+
95+
rows, err := DB.Query(ctx, queryString, args...)
96+
if err != nil {
97+
logrus.Errorf("query failed: %s\n\t%s", queryString, err)
98+
return nil, 0, err
99+
}
100+
defer rows.Close()
101+
102+
results := []string{}
103+
104+
for rows.Next() {
105+
r := model.Label{}
106+
107+
err = rows.Scan(&r.ID, &r.Key, &r.CreatedAt, &r.UpdatedAt, &r.LastPipelineReportAt)
108+
if err != nil {
109+
logrus.Errorf("scanning label row failed: %s", err)
110+
continue
111+
}
112+
113+
results = append(results, r.Key)
114+
}
115+
116+
if err := rows.Err(); err != nil {
117+
logrus.Errorf("iterating label rows failed: %s", err)
118+
return nil, 0, err
119+
}
120+
121+
return results, totalCount, nil
122+
}
123+
124+
// GetLabelRecords returns a list of labels from the labels database table.
125+
func GetLabelRecords(ctx context.Context, id, key, value, startTime, endTime string, limit, page int) ([]model.Label, int, error) {
126+
127+
query := psql.Select(
128+
sm.Columns("id", "key", "value", "created_at", "updated_at", "last_pipeline_report_at"),
129+
sm.From("labels"),
130+
sm.OrderBy("key"),
131+
)
132+
133+
if key != "" {
134+
query.Apply(
135+
sm.Where(psql.Quote("key").EQ(psql.Arg(key))),
136+
)
137+
}
138+
139+
if value != "" {
140+
query.Apply(
141+
sm.Where(psql.Quote("value").EQ(psql.Arg(value))),
142+
)
143+
}
144+
145+
if id != "" {
146+
query.Apply(
147+
sm.Where(psql.Quote("id").EQ(psql.Arg(id))),
148+
)
149+
}
150+
151+
if err := applyRangeFilter(
152+
"last_pipeline_report_at",
153+
dateRangeFilterParams{
154+
Query: &query,
155+
DateRangeDays: 0,
156+
StartTime: startTime,
157+
EndTime: endTime,
158+
}); err != nil {
159+
return nil, 0, fmt.Errorf("applying last_pipeline_report_at range filter: %w", err)
160+
}
161+
162+
totalCount := 0
163+
totalQuery := psql.Select(sm.From(query), sm.Columns("count(*)"))
164+
totalQueryString, totalArgs, err := totalQuery.Build(ctx)
165+
if err != nil {
166+
logrus.Errorf("building total count query failed: %s\n\t%s", totalQueryString, err)
167+
return nil, 0, err
168+
}
169+
170+
if err = DB.QueryRow(ctx, totalQueryString, totalArgs...).Scan(
171+
&totalCount,
172+
); err != nil {
173+
logrus.Errorf("parsing total count result: %s", err)
174+
}
175+
176+
if limit < totalCount && limit > 0 {
177+
query.Apply(
178+
sm.Limit(limit),
179+
sm.Offset((page-1)*limit),
180+
)
181+
}
182+
183+
queryString, args, err := query.Build(ctx)
184+
185+
if err != nil {
186+
logrus.Errorf("building query failed: %s\n\t%s", queryString, err)
187+
return nil, 0, err
188+
}
189+
190+
rows, err := DB.Query(ctx, queryString, args...)
191+
if err != nil {
192+
logrus.Errorf("query failed: %s\n\t%s", queryString, err)
193+
return nil, 0, err
194+
}
195+
defer rows.Close()
196+
197+
results := []model.Label{}
198+
199+
for rows.Next() {
200+
r := model.Label{}
201+
202+
err = rows.Scan(&r.ID, &r.Key, &r.Value, &r.CreatedAt, &r.UpdatedAt, &r.LastPipelineReportAt)
203+
if err != nil {
204+
logrus.Errorf("scanning label row failed: %s", err)
205+
continue
206+
}
207+
208+
results = append(results, r)
209+
}
210+
211+
if err := rows.Err(); err != nil {
212+
logrus.Errorf("iterating label rows failed: %s", err)
213+
return nil, 0, err
214+
}
215+
216+
return results, totalCount, nil
217+
}
218+
219+
// InitLabels takes a map of labels and ensures that they exist in the database, creating them if necessary.
220+
func InitLabels(ctx context.Context, labels map[string]string) ([]uuid.UUID, error) {
221+
errs := []error{}
222+
labelIDs := []uuid.UUID{}
223+
224+
for labelKey, labelValue := range labels {
225+
if labelKey == "" {
226+
errs = append(errs, fmt.Errorf("missing key, ignoring label:\t%q:%q", labelKey, labelValue))
227+
continue
228+
}
229+
230+
if labelValue == "" {
231+
errs = append(errs, fmt.Errorf("missing value, ignoring label:\t%q:%q", labelKey, labelValue))
232+
continue
233+
}
234+
235+
labelRecords, totalCount, err := GetLabelRecords(ctx, "", labelKey, labelValue, "", "", 0, 1)
236+
if err != nil {
237+
errs = append(errs, fmt.Errorf("failed to get labels: %s", err))
238+
continue
239+
}
240+
241+
switch totalCount {
242+
case 0:
243+
id, err := InsertLabel(ctx, labelKey, labelValue)
244+
if err != nil {
245+
err := fmt.Errorf("insert label data: %s", err)
246+
errs = append(errs, err)
247+
continue
248+
}
249+
250+
parsedID, err := uuid.Parse(id)
251+
if err != nil {
252+
errs = append(errs, fmt.Errorf("parsing id: %s", err))
253+
continue
254+
}
255+
256+
labelIDs = append(labelIDs, parsedID)
257+
case 1:
258+
if labelValue == labelRecords[0].Value {
259+
labelIDs = append(labelIDs, labelRecords[0].ID)
260+
}
261+
default:
262+
errMsg := fmt.Errorf("something went wrong multiple labels found for key %s", labelKey)
263+
logrus.Error(errMsg)
264+
errs = append(errs, errMsg)
265+
}
266+
}
267+
268+
if len(errs) > 0 {
269+
for i := range errs {
270+
logrus.Errorln(errs[i])
271+
}
272+
return nil, fmt.Errorf("something went wrong during label creation")
273+
}
274+
275+
return labelIDs, nil
276+
}

pkg/database/label_utils.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package database
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/stephenafamo/bob"
9+
"github.com/stephenafamo/bob/dialect/psql"
10+
"github.com/stephenafamo/bob/dialect/psql/dialect"
11+
"github.com/stephenafamo/bob/dialect/psql/sm"
12+
)
13+
14+
// labelFilterParams holds parameters for applying a date range filter to a query.
15+
type labelFilterParams struct {
16+
Query *bob.BaseQuery[*dialect.SelectQuery]
17+
Labels map[string]string
18+
StartTime string
19+
EndTime string
20+
Ctx context.Context
21+
}
22+
23+
func applyLabelFilter(params labelFilterParams) error {
24+
25+
if params.Ctx == nil {
26+
params.Ctx = context.Background()
27+
}
28+
29+
if len(params.Labels) == 0 {
30+
return nil
31+
}
32+
33+
errs := []error{}
34+
for key, value := range params.Labels {
35+
if key == "" {
36+
errs = append(errs, fmt.Errorf("label key cannot be empty"))
37+
continue
38+
}
39+
40+
results, totalCounts, err := GetLabelRecords(
41+
params.Ctx,
42+
"",
43+
key,
44+
value,
45+
params.StartTime,
46+
params.EndTime,
47+
0,
48+
1,
49+
)
50+
if err != nil {
51+
errs = append(errs, fmt.Errorf("failed getting label records: %s", err))
52+
continue
53+
}
54+
55+
if totalCounts == 0 {
56+
if value == "" {
57+
errs = append(errs, fmt.Errorf("label not found for key %s", key))
58+
} else {
59+
errs = append(errs, fmt.Errorf("label not found for %s=%s", key, value))
60+
}
61+
continue
62+
}
63+
64+
ids := make([]string, 0, len(results))
65+
for i := range results {
66+
ids = append(ids, results[i].ID.String())
67+
}
68+
69+
if len(ids) == 0 {
70+
if value == "" {
71+
errs = append(errs, fmt.Errorf("no label ids found for key %s", key))
72+
} else {
73+
errs = append(errs, fmt.Errorf("no label ids found for %s=%s", key, value))
74+
}
75+
continue
76+
}
77+
78+
params.Query.Apply(
79+
sm.Where(
80+
psql.Raw(`label_ids && ?`, fmt.Sprintf("{%s}", strings.Join(ids, ","))),
81+
),
82+
)
83+
}
84+
85+
if len(errs) > 0 {
86+
return fmt.Errorf("errors occurred while applying label filter: %v", errs)
87+
}
88+
89+
return nil
90+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DROP INDEX IF EXISTS idx_labels_key;
2+
DROP INDEX IF EXISTS idx_labels_value;
3+
ALTER TABLE IF EXISTS labels DROP CONSTRAINT IF EXISTS labels_key_value_unique;
4+
DROP TABLE IF EXISTS labels;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- This migration will create the labels table in the database, which will store the labels associated with the database.
2+
BEGIN;
3+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
4+
CREATE TABLE IF NOT EXISTS labels(
5+
id uuid DEFAULT uuid_generate_v4 (),
6+
key VARCHAR NOT NULL,
7+
value VARCHAR NOT NULL,
8+
created_at TIMESTAMP,
9+
updated_at TIMESTAMP,
10+
last_pipeline_report_at TIMESTAMP,
11+
CONSTRAINT labels_key_value_unique UNIQUE (key, value)
12+
);
13+
ALTER TABLE labels ALTER COLUMN created_at SET DEFAULT now();
14+
ALTER TABLE labels ALTER COLUMN updated_at SET DEFAULT now();
15+
16+
CREATE INDEX IF NOT EXISTS idx_labels_key ON labels (key);
17+
CREATE INDEX IF NOT EXISTS idx_labels_value ON labels (value);
18+
19+
COMMIT;

0 commit comments

Comments
 (0)