• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / minder / 11291822678

11 Oct 2024 11:51AM UTC coverage: 53.198% (+0.07%) from 53.133%
11291822678

push

github

web-flow
Remove database use from handleRelevantRepositoryEvent (#4705)

61 of 71 new or added lines in 4 files covered. (85.92%)

2 existing lines in 1 file now uncovered.

14621 of 27484 relevant lines covered (53.2%)

41.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.06
/internal/entities/handlers/handler.go
1
//
2
// Copyright 2024 Stacklok, Inc.
3
//
4
// Licensed under the Apache License, Version 2.0 (the "License");
5
// you may not use this file except in compliance with the License.
6
// You may obtain a copy of the License at
7
//
8
//     http://www.apache.org/licenses/LICENSE-2.0
9
//
10
// Unless required by applicable law or agreed to in writing, software
11
// distributed under the License is distributed on an "AS IS" BASIS,
12
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
// See the License for the specific language governing permissions and
14
// limitations under the License.
15

16
// Package handlers contains the message handlers for entities.
17
package handlers
18

19
import (
20
        "context"
21
        "errors"
22

23
        watermill "github.com/ThreeDotsLabs/watermill/message"
24
        "github.com/rs/zerolog"
25

26
        "github.com/stacklok/minder/internal/db"
27
        "github.com/stacklok/minder/internal/entities/handlers/message"
28
        "github.com/stacklok/minder/internal/entities/handlers/strategies"
29
        entStrategies "github.com/stacklok/minder/internal/entities/handlers/strategies/entity"
30
        msgStrategies "github.com/stacklok/minder/internal/entities/handlers/strategies/message"
31
        "github.com/stacklok/minder/internal/entities/models"
32
        "github.com/stacklok/minder/internal/entities/properties"
33
        propertyService "github.com/stacklok/minder/internal/entities/properties/service"
34
        "github.com/stacklok/minder/internal/events"
35
        "github.com/stacklok/minder/internal/projects/features"
36
        "github.com/stacklok/minder/internal/providers/manager"
37
        v1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
38
)
39

40
var (
41
        errPrivateRepoNotAllowed  = errors.New("private repositories are not allowed in this project")
42
        errArchivedRepoNotAllowed = errors.New("archived repositories are not evaluated")
43
        errPropsDoNotMatch        = errors.New("properties do not match")
44
)
45

46
type handleEntityAndDoBase struct {
47
        evt   events.Publisher
48
        store db.Store
49

50
        refreshEntity strategies.GetEntityStrategy
51
        createMessage strategies.MessageCreateStrategy
52

53
        handlerName        string
54
        forwardHandlerName string
55

56
        handlerMiddleware []watermill.HandlerMiddleware
57
}
58

59
// Register satisfies the events.Consumer interface.
60
func (b *handleEntityAndDoBase) Register(r events.Registrar) {
×
61
        r.Register(b.handlerName, b.handleRefreshEntityAndDo, b.handlerMiddleware...)
×
62
}
×
63

64
// handleRefreshEntityAndDo handles the refresh entity and forwarding a new message to the
65
// next handler. Creating the message and the way the entity is refreshed is determined by the
66
// strategies passed in.
67
//
68
// The handler doesn't retry on errors, it just logs them. We've had issues with retrying
69
// recently and it's unclear if there are any errors we /can/ retry on. We should identify
70
// errors to retry on and implement that in the future.
71
func (b *handleEntityAndDoBase) handleRefreshEntityAndDo(msg *watermill.Message) error {
15✔
72
        ctx := msg.Context()
15✔
73

15✔
74
        l := zerolog.Ctx(ctx).With().
15✔
75
                Str("messageStrategy", b.createMessage.GetName()).
15✔
76
                Str("refreshStrategy", b.refreshEntity.GetName()).
15✔
77
                Logger()
15✔
78

15✔
79
        // unmarshal the message
15✔
80
        entMsg, err := message.ToEntityRefreshAndDo(msg)
15✔
81
        if err != nil {
15✔
82
                l.Error().Err(err).Msg("error unpacking message")
×
83
                return nil
×
84
        }
×
85
        l.Debug().Msg("message unpacked")
15✔
86

15✔
87
        // call refreshEntity
15✔
88
        ewp, err := b.refreshEntity.GetEntity(ctx, entMsg)
15✔
89
        if err != nil {
19✔
90
                l.Error().Err(err).Msg("error refreshing entity")
4✔
91
                // do not return error in the handler, just log it
4✔
92
                // we might want to special-case retrying /some/ errors specifically those from the
4✔
93
                // provider, but for now, just log it
4✔
94
                return nil
4✔
95
        }
4✔
96

97
        if ewp != nil {
21✔
98
                l.Debug().
10✔
99
                        Str("entityID", ewp.Entity.ID.String()).
10✔
100
                        Str("providerID", ewp.Entity.ProviderID.String()).
10✔
101
                        Msg("entity refreshed")
10✔
102
        } else {
11✔
103
                l.Debug().Msg("entity not retrieved")
1✔
104
        }
1✔
105

106
        forward, err := b.forwardEntityCheck(ctx, entMsg, ewp)
11✔
107
        if err != nil {
11✔
NEW
108
                l.Error().Err(err).Msg("error checking entity")
×
NEW
109
                return nil
×
110
        } else if !forward {
14✔
111
                return nil
3✔
112
        }
3✔
113

114
        nextMsg, err := b.createMessage.CreateMessage(ctx, ewp)
8✔
115
        if err != nil {
9✔
116
                l.Error().Err(err).Msg("error creating message")
1✔
117
                return nil
1✔
118
        }
1✔
119

120
        // If nextMsg is nil, it means we don't need to publish anything (entity not found)
121
        if nextMsg != nil {
13✔
122
                l.Debug().Msg("publishing message")
6✔
123
                if err := b.evt.Publish(b.forwardHandlerName, nextMsg); err != nil {
6✔
124
                        l.Error().Err(err).Msg("error publishing message")
×
125
                        return nil
×
126
                }
×
127
        } else {
1✔
128
                l.Info().Msg("no message to publish")
1✔
129
        }
1✔
130

131
        return nil
7✔
132
}
133

134
func (b *handleEntityAndDoBase) forwardEntityCheck(
135
        ctx context.Context,
136
        entMsg *message.HandleEntityAndDoMessage,
137
        ewp *models.EntityWithProperties) (bool, error) {
11✔
138
        if ewp == nil {
12✔
139
                return true, nil
1✔
140
        }
1✔
141

142
        err := b.matchPropertiesCheck(entMsg, ewp)
10✔
143
        if errors.Is(err, errPropsDoNotMatch) {
11✔
144
                zerolog.Ctx(ctx).Debug().Err(err).Msg("properties do not match")
1✔
145
                return false, nil
1✔
146
        } else if err != nil {
10✔
NEW
147
                return false, err
×
NEW
148
        }
×
149

150
        err = b.repoPrivateOrArchivedCheck(ctx, ewp)
9✔
151
        if errors.Is(err, errPrivateRepoNotAllowed) || errors.Is(err, errArchivedRepoNotAllowed) {
11✔
152
                zerolog.Ctx(ctx).Debug().Err(err).Msg("private or archived repo")
2✔
153
                return false, nil
2✔
154
        } else if err != nil {
9✔
NEW
155
                return false, err
×
NEW
156
        }
×
157

158
        return true, nil
7✔
159
}
160

161
// matchPropertiesCheck checks if the properties of the entity match the properties in the message.
162
// this is different from the hint check, which is a check to see if the entity comes from where we expect
163
// it to come from. A concrete example is receiving a "meta" event on a webhook we don't manage, in that case
164
// we'd want to match on the webhook ID and only proceed if it matches with the webhook ID minder tracks.
165
func (_ *handleEntityAndDoBase) matchPropertiesCheck(
166
        entMsg *message.HandleEntityAndDoMessage,
167
        ewp *models.EntityWithProperties) error {
10✔
168
        // nothing to match against, so we're good
10✔
169
        if entMsg.MatchProps == nil {
18✔
170
                return nil
8✔
171
        }
8✔
172

173
        matchProps, err := properties.NewProperties(entMsg.MatchProps)
2✔
174
        if err != nil {
2✔
NEW
175
                return err
×
NEW
176
        }
×
177

178
        for propName, prop := range matchProps.Iterate() {
4✔
179
                entProp := ewp.Properties.GetProperty(propName)
2✔
180
                if !prop.Equal(entProp) {
3✔
181
                        return errPropsDoNotMatch
1✔
182
                }
1✔
183
        }
184

185
        return nil
1✔
186
}
187

188
func (b *handleEntityAndDoBase) repoPrivateOrArchivedCheck(
189
        ctx context.Context,
190
        ewp *models.EntityWithProperties) error {
9✔
191
        if ewp.Entity.Type == v1.Entity_ENTITY_REPOSITORIES &&
9✔
192
                ewp.Properties.GetProperty(properties.RepoPropertyIsPrivate).GetBool() &&
9✔
193
                !features.ProjectAllowsPrivateRepos(ctx, b.store, ewp.Entity.ProjectID) {
10✔
194
                return errPrivateRepoNotAllowed
1✔
195
        }
1✔
196

197
        if ewp.Entity.Type == v1.Entity_ENTITY_REPOSITORIES &&
8✔
198
                ewp.Properties.GetProperty(properties.RepoPropertyIsArchived).GetBool() {
9✔
199
                return errArchivedRepoNotAllowed
1✔
200
        }
1✔
201

202
        return nil
7✔
203
}
204

205
// NewRefreshByIDAndEvaluateHandler creates a new handler that refreshes an entity and evaluates it.
206
func NewRefreshByIDAndEvaluateHandler(
207
        evt events.Publisher,
208
        store db.Store,
209
        propSvc propertyService.PropertiesService,
210
        provMgr manager.ProviderManager,
211
        handlerMiddleware ...watermill.HandlerMiddleware,
212
) events.Consumer {
2✔
213
        return &handleEntityAndDoBase{
2✔
214
                evt:   evt,
2✔
215
                store: store,
2✔
216

2✔
217
                refreshEntity: entStrategies.NewRefreshEntityByIDStrategy(propSvc, provMgr, store),
2✔
218
                createMessage: msgStrategies.NewToEntityInfoWrapper(store, propSvc, provMgr),
2✔
219

2✔
220
                handlerName:        events.TopicQueueRefreshEntityByIDAndEvaluate,
2✔
221
                forwardHandlerName: events.TopicQueueEntityEvaluate,
2✔
222

2✔
223
                handlerMiddleware: handlerMiddleware,
2✔
224
        }
2✔
225
}
2✔
226

227
// NewRefreshEntityAndEvaluateHandler creates a new handler that refreshes an entity and evaluates it.
228
func NewRefreshEntityAndEvaluateHandler(
229
        evt events.Publisher,
230
        store db.Store,
231
        propSvc propertyService.PropertiesService,
232
        provMgr manager.ProviderManager,
233
        handlerMiddleware ...watermill.HandlerMiddleware,
234
) events.Consumer {
9✔
235
        return &handleEntityAndDoBase{
9✔
236
                evt:   evt,
9✔
237
                store: store,
9✔
238

9✔
239
                refreshEntity: entStrategies.NewRefreshEntityByUpstreamPropsStrategy(propSvc, provMgr, store),
9✔
240
                createMessage: msgStrategies.NewToEntityInfoWrapper(store, propSvc, provMgr),
9✔
241

9✔
242
                handlerName:        events.TopicQueueRefreshEntityAndEvaluate,
9✔
243
                forwardHandlerName: events.TopicQueueEntityEvaluate,
9✔
244

9✔
245
                handlerMiddleware: handlerMiddleware,
9✔
246
        }
9✔
247
}
9✔
248

249
// NewGetEntityAndDeleteHandler creates a new handler that gets an entity and deletes it.
250
func NewGetEntityAndDeleteHandler(
251
        evt events.Publisher,
252
        store db.Store,
253
        propSvc propertyService.PropertiesService,
254
        handlerMiddleware ...watermill.HandlerMiddleware,
255
) events.Consumer {
2✔
256
        return &handleEntityAndDoBase{
2✔
257
                evt:   evt,
2✔
258
                store: store,
2✔
259

2✔
260
                refreshEntity: entStrategies.NewGetEntityByUpstreamIDStrategy(propSvc),
2✔
261
                createMessage: msgStrategies.NewToMinderEntity(),
2✔
262

2✔
263
                handlerName:        events.TopicQueueGetEntityAndDelete,
2✔
264
                forwardHandlerName: events.TopicQueueReconcileEntityDelete,
2✔
265

2✔
266
                handlerMiddleware: handlerMiddleware,
2✔
267
        }
2✔
268
}
2✔
269

270
// NewAddOriginatingEntityHandler creates a new handler that adds an originating entity.
271
func NewAddOriginatingEntityHandler(
272
        evt events.Publisher,
273
        store db.Store,
274
        propSvc propertyService.PropertiesService,
275
        provMgr manager.ProviderManager,
276
        handlerMiddleware ...watermill.HandlerMiddleware,
277
) events.Consumer {
1✔
278
        return &handleEntityAndDoBase{
1✔
279
                evt:   evt,
1✔
280
                store: store,
1✔
281

1✔
282
                refreshEntity: entStrategies.NewAddOriginatingEntityStrategy(propSvc, provMgr, store),
1✔
283
                createMessage: msgStrategies.NewToEntityInfoWrapper(store, propSvc, provMgr),
1✔
284

1✔
285
                handlerName:        events.TopicQueueOriginatingEntityAdd,
1✔
286
                forwardHandlerName: events.TopicQueueEntityEvaluate,
1✔
287

1✔
288
                handlerMiddleware: handlerMiddleware,
1✔
289
        }
1✔
290
}
1✔
291

292
// NewRemoveOriginatingEntityHandler creates a new handler that removes an originating entity.
293
func NewRemoveOriginatingEntityHandler(
294
        evt events.Publisher,
295
        store db.Store,
296
        propSvc propertyService.PropertiesService,
297
        provMgr manager.ProviderManager,
298
        handlerMiddleware ...watermill.HandlerMiddleware,
299
) events.Consumer {
1✔
300
        return &handleEntityAndDoBase{
1✔
301
                evt: evt,
1✔
302

1✔
303
                refreshEntity: entStrategies.NewDelOriginatingEntityStrategy(propSvc, provMgr, store),
1✔
304
                createMessage: msgStrategies.NewCreateEmpty(),
1✔
305

1✔
306
                handlerName: events.TopicQueueOriginatingEntityDelete,
1✔
307

1✔
308
                handlerMiddleware: handlerMiddleware,
1✔
309
        }
1✔
310
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc