mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
Closes https://github.com/coder/internal/issues/369 We can't know whether a replacement (i.e. drift of terraform state leading to a resource needing to be deleted/recreated) will take place apriori; we can only detect it at `plan` time, because the provider decides whether a resource must be replaced and it cannot be inferred through static analysis of the template. **This is likely to be the most common gotcha with using prebuilds, since it requires a slight template modification to use prebuilds effectively**, so let's head this off before it's an issue for customers. Drift details will now be logged in the workspace build logs:  Plus a notification will be sent to template admins when this situation arises:  A new metric - `coderd_prebuilt_workspaces_resource_replacements_total` - will also increment each time a workspace encounters replacements. We only track _that_ a resource replacement occurred, not how many. Just one is enough to ruin a prebuild, but we can't know apriori which replacement would cause this. For example, say we have 2 replacements: a `docker_container` and a `null_resource`; we don't know which one might cause an issue (or indeed if either would), so we just track the replacement. --------- Signed-off-by: Danny Kopping <dannykopping@gmail.com>
130 lines
3.5 KiB
Go
130 lines
3.5 KiB
Go
package notificationstest
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/notifications"
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
)
|
|
|
|
type FakeEnqueuer struct {
|
|
authorizer rbac.Authorizer
|
|
mu sync.Mutex
|
|
sent []*FakeNotification
|
|
}
|
|
|
|
var _ notifications.Enqueuer = &FakeEnqueuer{}
|
|
|
|
func NewFakeEnqueuer() *FakeEnqueuer {
|
|
return &FakeEnqueuer{}
|
|
}
|
|
|
|
type FakeNotification struct {
|
|
UserID, TemplateID uuid.UUID
|
|
Labels map[string]string
|
|
Data map[string]any
|
|
CreatedBy string
|
|
Targets []uuid.UUID
|
|
}
|
|
|
|
// TODO: replace this with actual calls to dbauthz.
|
|
// See: https://github.com/coder/coder/issues/15481
|
|
func (f *FakeEnqueuer) assertRBACNoLock(ctx context.Context) {
|
|
if f.mu.TryLock() {
|
|
panic("Developer error: do not call assertRBACNoLock outside of a mutex lock!")
|
|
}
|
|
|
|
// If we get here, we are locked.
|
|
if f.authorizer == nil {
|
|
f.authorizer = rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
|
|
}
|
|
|
|
act, ok := dbauthz.ActorFromContext(ctx)
|
|
if !ok {
|
|
panic("Developer error: no actor in context, you may need to use dbauthz.AsNotifier(ctx)")
|
|
}
|
|
|
|
for _, a := range []policy.Action{policy.ActionCreate, policy.ActionRead} {
|
|
err := f.authorizer.Authorize(ctx, act, a, rbac.ResourceNotificationMessage)
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
if rbac.IsUnauthorizedError(err) {
|
|
panic(fmt.Sprintf("Developer error: not authorized to %s %s. "+
|
|
"Ensure that you are using dbauthz.AsXXX with an actor that has "+
|
|
"policy.ActionCreate on rbac.ResourceNotificationMessage", a, rbac.ResourceNotificationMessage.Type))
|
|
}
|
|
panic("Developer error: failed to check auth:" + err.Error())
|
|
}
|
|
}
|
|
|
|
func (f *FakeEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) ([]uuid.UUID, error) {
|
|
return f.EnqueueWithData(ctx, userID, templateID, labels, nil, createdBy, targets...)
|
|
}
|
|
|
|
func (f *FakeEnqueuer) EnqueueWithData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) ([]uuid.UUID, error) {
|
|
return f.enqueueWithDataLock(ctx, userID, templateID, labels, data, createdBy, targets...)
|
|
}
|
|
|
|
func (f *FakeEnqueuer) enqueueWithDataLock(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) ([]uuid.UUID, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.assertRBACNoLock(ctx)
|
|
|
|
f.sent = append(f.sent, &FakeNotification{
|
|
UserID: userID,
|
|
TemplateID: templateID,
|
|
Labels: labels,
|
|
Data: data,
|
|
CreatedBy: createdBy,
|
|
Targets: targets,
|
|
})
|
|
|
|
id := uuid.New()
|
|
return []uuid.UUID{id}, nil
|
|
}
|
|
|
|
func (f *FakeEnqueuer) Clear() {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
f.sent = nil
|
|
}
|
|
|
|
func (f *FakeEnqueuer) Sent(matchers ...func(*FakeNotification) bool) []*FakeNotification {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
sent := []*FakeNotification{}
|
|
for _, notif := range f.sent {
|
|
// Check this notification matches all given matchers
|
|
matches := true
|
|
for _, matcher := range matchers {
|
|
if !matcher(notif) {
|
|
matches = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if matches {
|
|
sent = append(sent, notif)
|
|
}
|
|
}
|
|
|
|
return sent
|
|
}
|
|
|
|
func WithTemplateID(id uuid.UUID) func(*FakeNotification) bool {
|
|
return func(n *FakeNotification) bool {
|
|
return n.TemplateID == id
|
|
}
|
|
}
|