-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Project deletion metadata audit log #4262
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are other paths which can delete projects, in particular at least https://github.com/stacklok/minder/blob/main/internal/projects/deleter.go#L70 and https://github.com/stacklok/minder/blob/main/internal/projects/deleter.go#L117.
There's a path via https://github.com/stacklok/minder/blob/main/internal/controlplane/identity_events.go#L81 where users who delete their Stacklok IDP accounts (e.g. in Keycloak) will have their resources automatically cleaned up. That seems to call the non-struct version of DeleteUser, which is also called by the DeleteUser grpc endpoint.
internal/logger/telemetry_store.go
Outdated
// ProjectMetadata can be used to store project metadata in the context. | ||
type ProjectMetadata struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we call this "ProjectTombstone" or "ProjectDeletion", so it's clear that this is specifically for the case when a project is deleted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching that and my bad for the obvious miss!
I've added the log audit at the core function which seems to be called from all other paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Realized the previous comment should have been as "request changes")
var err error | ||
entitlements, err = qtx.GetEntitlementsByProjectID(ctx, projectID) | ||
return err | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If these calls aren't used anymore, can you clean these up from the database queries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are, in internal/projects/deleter.go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we care about this functionality, we should add a test for it.
internal/projects/deleter.go
Outdated
g, ctx := errgroup.WithContext(ctx) | ||
g.Go(func() error { | ||
var err error | ||
profilesCount, err = qtx.CountProfilesByProjectID(ctx, projectID) | ||
return err | ||
}) | ||
g.Go(func() error { | ||
var err error | ||
reposCount, err = qtx.CountRepositoriesByProjectID(ctx, projectID) | ||
return err | ||
}) | ||
g.Go(func() error { | ||
var err error | ||
entitlements, err = qtx.GetEntitlementsByProjectID(ctx, projectID) | ||
return err | ||
}) | ||
|
||
if err := g.Wait(); err != nil { | ||
return nil, fmt.Errorf("error getting project metadata: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth the effort to parallelize these three queries?
internal/projects/deleter.go
Outdated
) | ||
|
||
g, ctx := errgroup.WithContext(ctx) | ||
g.Go(func() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you use a named argument, this would be much shorter:
g.Go(func() err error {
profilesCount, err = qtx.CountProfilesByProjectID(ctx, projectID)
}
internal/projects/deleter.go
Outdated
return nil, fmt.Errorf("error getting project metadata: %w", err) | ||
} | ||
|
||
var entitlementsFeatures []string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use make
with a size argument to avoid needing extra resizes:
var entitlementsFeatures []string | |
entitlementsFeatures := make([]string, 0, len(entitlements)) |
internal/projects/deleter_test.go
Outdated
mockStore.EXPECT().CountProfilesByProjectID(gomock.Any(), proj). | ||
Return(int64(0), nil) | ||
mockStore.EXPECT().CountRepositoriesByProjectID(gomock.Any(), proj). | ||
Return(int64(0), nil) | ||
mockStore.EXPECT().GetEntitlementsByProjectID(gomock.Any(), proj). | ||
Return([]db.Entitlement{}, nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add at least one test where you verify that these are getting added to the telemetry context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple very minor simplifications, but I think this can merge as-is.
internal/logger/telemetry_store.go
Outdated
return pt.Project == other.Project && | ||
pt.ProfileCount == other.ProfileCount && | ||
pt.RepositoriesCount == other.RepositoriesCount && | ||
len(pt.Entitlements) == len(other.Entitlements) && | ||
func() bool { | ||
for i, v := range pt.Entitlements { | ||
if v != other.Entitlements[i] { | ||
return false | ||
} | ||
} | ||
return true | ||
}() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably simpler as:
return pt.Project == other.Project && | |
pt.ProfileCount == other.ProfileCount && | |
pt.RepositoriesCount == other.RepositoriesCount && | |
len(pt.Entitlements) == len(other.Entitlements) && | |
func() bool { | |
for i, v := range pt.Entitlements { | |
if v != other.Entitlements[i] { | |
return false | |
} | |
} | |
return true | |
}() | |
for i, v := range pt.Entitlements { | |
if v != other.Entitlements[i] { | |
return false | |
} | |
} | |
return pt.Project == other.Project && | |
pt.ProfileCount == other.ProfileCount && | |
pt.RepositoriesCount == other.RepositoriesCount && | |
len(pt.Entitlements) == len(other.Entitlements) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True -> updated
There is a potential out-of-bounds issue with the suggested code so I changed it a bit.
database/query/entitlements.sql
Outdated
FROM entitlements | ||
WHERE project_id = sqlc.arg(project_id)::UUID; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing if this was SELECT feature FROM entitlements
you still wouldn't get a []string
as the return type...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It actually does return a []string
. I did it this way to be more generic and reusable in the future but if you think it's better for this one to only return a slice of the feature names then I'm ok with it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks
internal/logger/telemetry_store.go
Outdated
if len(pt.Entitlements) != len(other.Entitlements) { | ||
return false | ||
} | ||
|
||
for i, v := range pt.Entitlements { | ||
if v != other.Entitlements[i] { | ||
return false | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooops, I just realized that there is slices.Equal
that should do all this.
Go ahead and merge without |
Summary
Provide a brief overview of the changes and the issue being addressed.
Explain the rationale and any background necessary for understanding the changes.
List dependencies required by this change, if any.
In order for us to be able to calculate customer churn, we need to keep track of the deleted projects/accounts on Minder and the relevant metadata from a business perspective.
Fixes https://app.zenhub.com/workspaces/minder-sprints-666857b34fcef725d2efba7c/issues/gh/stacklok/infra/1906
Change Type
Mark the type of change your PR introduces:
Testing
Outline how the changes were tested, including steps to reproduce and any relevant configurations.
Attach screenshots if helpful.
Review Checklist: