codeintel: Support bulk deletion of LSIF uploads (#42395)

This commit is contained in:
Chris Wendt 2022-10-03 20:17:19 -06:00 committed by GitHub
parent 9fa0f29637
commit d3268e7b9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 640 additions and 17 deletions

View File

@ -95,7 +95,7 @@ interface FilteredConnectionProps<C extends Connection<N>, N, NP = {}, HP = {}>
queryConnection: (args: FilteredConnectionQueryArguments) => Observable<C>
/** Called when the queryConnection Observable emits. */
onUpdate?: (value: C | ErrorLike | undefined, query: string) => void
onUpdate?: (value: C | ErrorLike | undefined, query: string, activeValues: FilteredConnectionArgs) => void
/**
* Set to true when the GraphQL response is expected to emit an `PageInfo.endCursor` value when
@ -359,7 +359,11 @@ export class FilteredConnection<
}
}
if (this.props.onUpdate) {
this.props.onUpdate(connectionOrError, this.state.query)
this.props.onUpdate(
connectionOrError,
this.state.query,
this.buildArgs(this.state.activeValues)
)
}
this.setState({ connectionOrError, ...rest })
},
@ -560,6 +564,7 @@ export class FilteredConnection<
}
private onChange: React.ChangeEventHandler<HTMLInputElement> = event => {
this.props.onInputChange?.(event)
this.queryInputChanges.next(event.currentTarget.value)
}
@ -576,10 +581,8 @@ export class FilteredConnection<
this.showMoreClicks.next()
}
private buildArgs = (
values: Map<string, FilteredConnectionFilterValue>
): { [name: string]: string | number | boolean } => {
let args: { [name: string]: string | number | boolean } = {}
private buildArgs = (values: Map<string, FilteredConnectionFilterValue>): FilteredConnectionArgs => {
let args: FilteredConnectionArgs = {}
for (const key of values.keys()) {
const value = values.get(key)
if (value === undefined) {
@ -601,3 +604,7 @@ export const resetFilteredConnectionURLQuery = (parameters: URLSearchParams): vo
parameters.delete('first')
parameters.delete('after')
}
export interface FilteredConnectionArgs {
[name: string]: string | number | boolean
}

View File

@ -3,7 +3,7 @@ import React, { FunctionComponent } from 'react'
import { mdiChevronRight } from '@mdi/js'
import classNames from 'classnames'
import { Link, H3, Icon } from '@sourcegraph/wildcard'
import { Link, H3, Icon, Checkbox } from '@sourcegraph/wildcard'
import { LsifUploadFields } from '../../../../graphql-operations'
import { CodeIntelState } from '../../shared/components/CodeIntelState'
@ -19,14 +19,25 @@ import styles from './CodeIntelUploadNode.module.scss'
export interface CodeIntelUploadNodeProps {
node: LsifUploadFields
now?: () => Date
selection: Set<string> | 'all'
onCheckboxToggle: (id: string, checked: boolean) => void
}
export const CodeIntelUploadNode: FunctionComponent<React.PropsWithChildren<CodeIntelUploadNodeProps>> = ({
node,
now,
selection,
onCheckboxToggle,
}) => (
<>
<span className={styles.separator} />
<Checkbox
label=""
id="disabledFieldsetCheck"
disabled={selection === 'all'}
checked={selection === 'all' ? true : selection.has(node.id)}
onChange={input => onCheckboxToggle(node.id, input.target.checked)}
/>
<div className={classNames(styles.information, 'd-flex flex-column')}>
<div className="m-0">

View File

@ -0,0 +1,34 @@
import { ApolloError, MutationFunctionOptions, FetchResult, useMutation } from '@apollo/client'
import { gql, getDocumentNode } from '@sourcegraph/http-client'
import { DeleteLsifUploadsResult, DeleteLsifUploadsVariables } from '../../../../graphql-operations'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type DeleteLsifUploadsResults = Promise<FetchResult<DeleteLsifUploadsResult, Record<string, any>, Record<string, any>>>
interface UseDeleteLsifUploadsResult {
handleDeleteLsifUploads: (
options?: MutationFunctionOptions<DeleteLsifUploadsResult, DeleteLsifUploadsVariables> | undefined
) => DeleteLsifUploadsResults
deletesError: ApolloError | undefined
}
const DELETE_LSIF_UPLOAD = gql`
mutation DeleteLsifUploads($query: String, $state: LSIFUploadState, $isLatestForRepo: Boolean, $repository: ID) {
deleteLSIFUploads(query: $query, state: $state, isLatestForRepo: $isLatestForRepo, repository: $repository) {
alwaysNil
}
}
`
export const useDeleteLsifUploads = (): UseDeleteLsifUploadsResult => {
const [handleDeleteLsifUploads, { error }] = useMutation<DeleteLsifUploadsResult, DeleteLsifUploadsVariables>(
getDocumentNode(DELETE_LSIF_UPLOAD)
)
return {
handleDeleteLsifUploads,
deletesError: error,
}
}

View File

@ -1,6 +1,6 @@
.grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
grid-template-columns: min-content [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;

View File

@ -3,10 +3,12 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 're
import { useApolloClient } from '@apollo/client'
import classNames from 'classnames'
import { RouteComponentProps, useLocation } from 'react-router'
import { of } from 'rxjs'
import { of, Subject } from 'rxjs'
import { ErrorAlert } from '@sourcegraph/branded/src/components/alerts'
import { isErrorLike } from '@sourcegraph/common'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { Container, PageHeader, useObservable } from '@sourcegraph/wildcard'
import { Button, Container, PageHeader, useObservable } from '@sourcegraph/wildcard'
import {
FilteredConnection,
@ -14,7 +16,7 @@ import {
FilteredConnectionQueryArguments,
} from '../../../../components/FilteredConnection'
import { PageTitle } from '../../../../components/PageTitle'
import { LsifUploadFields, LSIFUploadState } from '../../../../graphql-operations'
import { DeleteLsifUploadsVariables, LsifUploadFields, LSIFUploadState } from '../../../../graphql-operations'
import { FlashMessage } from '../../configuration/components/FlashMessage'
import { queryCommitGraphMetadata as defaultQueryCommitGraphMetadata } from '../../indexes/hooks/queryCommitGraphMetadata'
import { CodeIntelUploadNode, CodeIntelUploadNodeProps } from '../components/CodeIntelUploadNode'
@ -22,6 +24,8 @@ import { CommitGraphMetadata } from '../components/CommitGraphMetadata'
import { EmptyUploads } from '../components/EmptyUploads'
import { queryLsifUploadsByRepository as defaultQueryLsifUploadsByRepository } from '../hooks/queryLsifUploadsByRepository'
import { queryLsifUploadsList as defaultQueryLsifUploadsList } from '../hooks/queryLsifUploadsList'
import { useDeleteLsifUpload } from '../hooks/useDeleteLsifUpload'
import { useDeleteLsifUploads } from '../hooks/useDeleteLsifUploads'
import styles from './CodeIntelUploadsPage.module.scss'
@ -134,6 +138,29 @@ export const CodeIntelUploadsPage: FunctionComponent<React.PropsWithChildren<Cod
}
}, [location.state])
const [args, setArgs] = useState<DeleteLsifUploadsVariables>()
// selection has the same type as CodeIntelUploadNode's prop because there is no CodeIntelUploadNodeProps
const [selection, setSelection] = useState<CodeIntelUploadNodeProps['selection']>(new Set())
const onCheckboxToggle = useCallback<CodeIntelUploadNodeProps['onCheckboxToggle']>((id, checked) => {
setSelection(selection => {
if (selection === 'all') {
return selection
}
if (checked) {
selection.add(id)
} else {
selection.delete(id)
}
return new Set(selection)
})
}, [])
const { handleDeleteLsifUpload, deleteError } = useDeleteLsifUpload()
const { handleDeleteLsifUploads, deletesError } = useDeleteLsifUploads()
const deletes = useMemo(() => new Subject<undefined>(), [])
return (
<div className="code-intel-uploads">
<PageTitle title="Code graph data uploads" />
@ -164,6 +191,61 @@ export const CodeIntelUploadsPage: FunctionComponent<React.PropsWithChildren<Cod
)}
<Container>
<div className="mb-3">
<Button
className="mr-2"
variant="primary"
disabled={selection !== 'all' && selection.size === 0}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={async () => {
if (selection === 'all') {
if (args === undefined) {
return
}
if (
!confirm(
`Delete all uploads matching the filter criteria?\n\n${Object.entries(args)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')}`
)
) {
return
}
await handleDeleteLsifUploads({
variables: args,
update: cache => cache.modify({ fields: { node: () => {} } }),
})
deletes.next()
return
}
for (const id of selection) {
await handleDeleteLsifUpload({
variables: { id },
update: cache => cache.modify({ fields: { node: () => {} } }),
})
}
deletes.next()
}}
>
Delete {selection === 'all' ? 'matching' : selection.size === 0 ? '' : selection.size}
</Button>
<Button
variant="secondary"
onClick={() => setSelection(selection => (selection === 'all' ? new Set() : 'all'))}
>
{selection === 'all' ? 'Deselect' : 'Select matching'}
</Button>
</div>
{isErrorLike(deleteError) && <ErrorAlert prefix="Error deleting LSIF upload" error={deleteError} />}
{isErrorLike(deletesError) && <ErrorAlert prefix="Error deleting LSIF uploads" error={deletesError} />}
<div className="list-group position-relative">
<FilteredConnection<LsifUploadFields, Omit<CodeIntelUploadNodeProps, 'node'>>
listComponent="div"
@ -172,13 +254,25 @@ export const CodeIntelUploadsPage: FunctionComponent<React.PropsWithChildren<Cod
noun="upload"
pluralNoun="uploads"
nodeComponent={CodeIntelUploadNode}
nodeComponentProps={{ now }}
queryConnection={queryLsifUploads}
nodeComponentProps={{ now, selection, onCheckboxToggle }}
queryConnection={args => {
setArgs({
query: args.query ?? null,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
state: (args as any).state ?? null,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
isLatestForRepo: (args as any).isLatestForRepo ?? null,
repository: repo?.id ?? null,
})
setSelection(new Set())
return queryLsifUploads(args)
}}
history={history}
location={props.location}
cursorPaging={true}
filters={filters}
emptyElement={<EmptyUploads />}
updates={deletes}
/>
</div>
</Container>

View File

@ -50,6 +50,7 @@ type UploadsServiceResolver interface {
LSIFUploads(ctx context.Context, args *uploadsgraphql.LSIFUploadsQueryArgs) (resolvers.LSIFUploadConnectionResolver, error)
LSIFUploadsByRepo(ctx context.Context, args *uploadsgraphql.LSIFRepositoryUploadsQueryArgs) (resolvers.LSIFUploadConnectionResolver, error)
DeleteLSIFUpload(ctx context.Context, args *struct{ ID graphql.ID }) (*resolvers.EmptyResponse, error)
DeleteLSIFUploads(ctx context.Context, args *uploadsgraphql.DeleteLSIFUploadsArgs) (*resolvers.EmptyResponse, error)
}
type PoliciesServiceResolver interface {
CodeIntelligenceConfigurationPolicies(ctx context.Context, args *policiesgraphql.CodeIntelligenceConfigurationPoliciesArgs) (policiesgraphql.CodeIntelligenceConfigurationPolicyConnectionResolver, error)

View File

@ -71,6 +71,32 @@ extend type Mutation {
"""
deleteLSIFUpload(id: ID!): EmptyResponse
"""
Deletes LSIF uploads by filter criteria.
"""
deleteLSIFUploads(
"""
An (optional) search query that filters the state, repository name,
commit, root, and indexer properties.
"""
query: String
"""
The upload state.
"""
state: LSIFUploadState
"""
When specified, only deletes uploads that are latest for the given repository.
"""
isLatestForRepo: Boolean
"""
The repository.
"""
repository: ID
): EmptyResponse
"""
Deletes an LSIF index.
"""

View File

@ -70,6 +70,11 @@ func (r *Resolver) DeleteLSIFUpload(ctx context.Context, args *struct{ ID graphq
return r.resolver.UploadRootResolver().DeleteLSIFUpload(ctx, args)
}
// 🚨 SECURITY: Only site admins may modify code intelligence upload data
func (r *Resolver) DeleteLSIFUploads(ctx context.Context, args *uploadsgraphql.DeleteLSIFUploadsArgs) (_ *sharedresolvers.EmptyResponse, err error) {
return r.resolver.UploadRootResolver().DeleteLSIFUploads(ctx, args)
}
// 🚨 SECURITY: dbstore layer handles authz for GetIndexByID
func (r *Resolver) LSIFIndexByID(ctx context.Context, id graphql.ID) (_ sharedresolvers.LSIFIndexResolver, err error) {
return r.resolver.AutoIndexingRootResolver().LSIFIndexByID(ctx, id)

View File

@ -120,6 +120,13 @@ type GetUploadsOptions struct {
InCommitGraph bool
}
type DeleteUploadsOptions struct {
State string
Term string
VisibleAtTip bool
RepositoryID int
}
type GetConfigurationPoliciesOptions struct {
// RepositoryID indicates that only configuration policies that apply to the
// specified repository (directly or via pattern) should be returned. This value

View File

@ -56,6 +56,7 @@ type operations struct {
addUploadPart *observation.Operation
markQueued *observation.Operation
markFailed *observation.Operation
deleteUploads *observation.Operation
// Dumps
findClosestDumps *observation.Operation
@ -139,6 +140,7 @@ func newOperations(observationContext *observation.Context) *operations {
addUploadPart: op("AddUploadPart"),
markQueued: op("MarkQueued"),
markFailed: op("MarkFailed"),
deleteUploads: op("DeleteUploads"),
writeVisibleUploads: op("writeVisibleUploads"),
persistNearestUploads: op("persistNearestUploads"),

View File

@ -61,6 +61,7 @@ type Store interface {
DeleteUploadsStuckUploading(ctx context.Context, uploadedBefore time.Time) (_ int, err error)
DeleteUploadsWithoutRepository(ctx context.Context, now time.Time) (_ map[int]int, err error)
DeleteUploadByID(ctx context.Context, id int) (_ bool, err error)
DeleteUploads(ctx context.Context, opts types.DeleteUploadsOptions) (err error)
// Uploads (uploading)
InsertUpload(ctx context.Context, upload types.Upload) (int, error)

View File

@ -28,10 +28,10 @@ import (
// GetUploads returns a list of uploads and the total count of records matching the given conditions.
func (s *store) GetUploads(ctx context.Context, opts types.GetUploadsOptions) (uploads []types.Upload, totalCount int, err error) {
ctx, trace, endObservation := s.operations.getUploads.With(ctx, &err, observation.Args{LogFields: buildLogFields(opts)})
ctx, trace, endObservation := s.operations.getUploads.With(ctx, &err, observation.Args{LogFields: buildGetUploadsLogFields(opts)})
defer endObservation(1, observation.Args{})
tableExpr, conds, cte := buildConditionsAndCte(opts)
tableExpr, conds, cte := buildGetConditionsAndCte(opts)
authzConds, err := database.AuthzQueryConds(ctx, database.NewDBWith(s.logger, s.db))
if err != nil {
return nil, 0, err
@ -679,6 +679,59 @@ const deleteUploadByIDQuery = `
UPDATE lsif_uploads u SET state = CASE WHEN u.state = 'completed' THEN 'deleting' ELSE 'deleted' END WHERE id = %s RETURNING repository_id
`
// DeleteUploads deletes uploads by filter criteria. The associated repositories will be marked as dirty
// so that their commit graphs will be updated in the background.
func (s *store) DeleteUploads(ctx context.Context, opts types.DeleteUploadsOptions) (err error) {
ctx, _, endObservation := s.operations.deleteUploads.With(ctx, &err, observation.Args{LogFields: buildDeleteUploadsLogFields(opts)})
defer endObservation(1, observation.Args{})
conds := buildDeleteConditions(opts)
authzConds, err := database.AuthzQueryConds(ctx, database.NewDBWith(s.logger, s.db))
if err != nil {
return err
}
conds = append(conds, authzConds)
tx, err := s.transact(ctx)
if err != nil {
return err
}
defer func() { err = tx.Done(err) }()
unset, _ := tx.db.SetLocal(ctx, "codeintel.lsif_uploads_audit.reason", "direct delete by filter criteria request")
defer unset(ctx)
query := sqlf.Sprintf(
deleteUploadsQuery,
sqlf.Join(conds, " AND "),
)
repoIDs, err := basestore.ScanInts(s.db.Query(ctx, query))
if err != nil {
return err
}
var dirtyErr error
for _, repoID := range repoIDs {
if err := tx.SetRepositoryAsDirty(ctx, repoID); err != nil {
dirtyErr = err
}
}
if dirtyErr != nil {
err = dirtyErr
}
return err
}
const deleteUploadsQuery = `
-- source: internal/codeintel/stores/dbstore/uploads.go:DeleteUploads
UPDATE lsif_uploads u
SET state = CASE WHEN u.state = 'completed' THEN 'deleting' ELSE 'deleted' END
FROM repo
WHERE repo.id = u.repository_id AND %s
RETURNING repository_id
`
// UpdateUploadRetention updates the last data retention scan timestamp on the upload
// records with the given protected identifiers and sets the expired field on the upload
// records with the given expired identifiers.
@ -1931,7 +1984,7 @@ func nilTimeToString(t *time.Time) string {
return t.String()
}
func buildConditionsAndCte(opts types.GetUploadsOptions) (*sqlf.Query, []*sqlf.Query, []cteDefinition) {
func buildGetConditionsAndCte(opts types.GetUploadsOptions) (*sqlf.Query, []*sqlf.Query, []cteDefinition) {
conds := make([]*sqlf.Query, 0, 12)
allowDeletedUploads := (opts.AllowDeletedUpload && opts.State == "") || opts.State == "deleted"
@ -2047,6 +2100,26 @@ func buildConditionsAndCte(opts types.GetUploadsOptions) (*sqlf.Query, []*sqlf.Q
return sourceTableExpr, conds, cteDefinitions
}
func buildDeleteConditions(opts types.DeleteUploadsOptions) []*sqlf.Query {
conds := []*sqlf.Query{}
if opts.RepositoryID != 0 {
conds = append(conds, sqlf.Sprintf("u.repository_id = %s", opts.RepositoryID))
}
conds = append(conds, sqlf.Sprintf("repo.deleted_at IS NULL"))
conds = append(conds, sqlf.Sprintf("u.state != 'deleted'"))
if opts.Term != "" {
conds = append(conds, makeSearchCondition(opts.Term))
}
if opts.State != "" {
conds = append(conds, makeStateCondition(opts.State))
}
if opts.VisibleAtTip {
conds = append(conds, sqlf.Sprintf("EXISTS ("+visibleAtTipSubselectQuery+")"))
}
return conds
}
// makeSearchCondition returns a disjunction of LIKE clauses against all searchable columns of an upload.
func makeSearchCondition(term string) *sqlf.Query {
searchableColumns := []string{
@ -2098,7 +2171,7 @@ func buildCTEPrefix(cteDefinitions []cteDefinition) *sqlf.Query {
return sqlf.Sprintf("WITH\n%s", sqlf.Join(cteQueries, ",\n"))
}
func buildLogFields(opts types.GetUploadsOptions) []log.Field {
func buildGetUploadsLogFields(opts types.GetUploadsOptions) []log.Field {
return []log.Field{
log.Int("repositoryID", opts.RepositoryID),
log.String("state", opts.State),
@ -2116,3 +2189,11 @@ func buildLogFields(opts types.GetUploadsOptions) []log.Field {
log.Int("offset", opts.Offset),
}
}
func buildDeleteUploadsLogFields(opts types.DeleteUploadsOptions) []log.Field {
return []log.Field{
log.String("state", opts.State),
log.String("term", opts.Term),
log.Bool("visibleAtTip", opts.VisibleAtTip),
}
}

View File

@ -638,6 +638,55 @@ func TestDeleteUploadsStuckUploading(t *testing.T) {
}
}
func TestDeleteUploads(t *testing.T) {
logger := logtest.Scoped(t)
db := database.NewDB(logger, dbtest.NewDB(logger, t))
store := New(db, &observation.TestContext)
t1 := time.Unix(1587396557, 0).UTC()
t2 := t1.Add(time.Minute * 1)
t3 := t1.Add(time.Minute * 2)
t4 := t1.Add(time.Minute * 3)
t5 := t1.Add(time.Minute * 4)
insertUploads(t, db,
types.Upload{ID: 1, Commit: makeCommit(1111), UploadedAt: t1, State: "queued"}, // will not be deleted
types.Upload{ID: 2, Commit: makeCommit(1112), UploadedAt: t2, State: "uploading"}, // will be deleted
types.Upload{ID: 3, Commit: makeCommit(1113), UploadedAt: t3, State: "uploading"}, // will be deleted
types.Upload{ID: 4, Commit: makeCommit(1114), UploadedAt: t4, State: "completed"}, // will not be deleted
types.Upload{ID: 5, Commit: makeCommit(1115), UploadedAt: t5, State: "uploading"}, // will be deleted
)
err := store.DeleteUploads(context.Background(), types.DeleteUploadsOptions{
State: "uploading",
Term: "",
VisibleAtTip: false,
})
if err != nil {
t.Fatalf("unexpected error deleting uploads: %s", err)
}
uploads, totalCount, err := store.GetUploads(context.Background(), types.GetUploadsOptions{Limit: 5})
if err != nil {
t.Fatalf("unexpected error getting uploads: %s", err)
}
var ids []int
for _, upload := range uploads {
ids = append(ids, upload.ID)
}
sort.Ints(ids)
expectedIDs := []int{1, 4}
if totalCount != len(expectedIDs) {
t.Errorf("unexpected total count. want=%d have=%d", len(expectedIDs), totalCount)
}
if diff := cmp.Diff(expectedIDs, ids); diff != "" {
t.Errorf("unexpected upload ids (-want +got):\n%s", diff)
}
}
func TestHardDeleteUploadsByIDs(t *testing.T) {
logger := logtest.Scoped(t)
db := database.NewDB(logger, dbtest.NewDB(logger, t))

View File

@ -48,6 +48,9 @@ type MockStore struct {
// DeleteUploadByIDFunc is an instance of a mock function object
// controlling the behavior of the method DeleteUploadByID.
DeleteUploadByIDFunc *StoreDeleteUploadByIDFunc
// DeleteUploadsFunc is an instance of a mock function object
// controlling the behavior of the method DeleteUploads.
DeleteUploadsFunc *StoreDeleteUploadsFunc
// DeleteUploadsStuckUploadingFunc is an instance of a mock function
// object controlling the behavior of the method
// DeleteUploadsStuckUploading.
@ -238,6 +241,11 @@ func NewMockStore() *MockStore {
return
},
},
DeleteUploadsFunc: &StoreDeleteUploadsFunc{
defaultHook: func(context.Context, types.DeleteUploadsOptions) (r0 error) {
return
},
},
DeleteUploadsStuckUploadingFunc: &StoreDeleteUploadsStuckUploadingFunc{
defaultHook: func(context.Context, time.Time) (r0 int, r1 error) {
return
@ -505,6 +513,11 @@ func NewStrictMockStore() *MockStore {
panic("unexpected invocation of MockStore.DeleteUploadByID")
},
},
DeleteUploadsFunc: &StoreDeleteUploadsFunc{
defaultHook: func(context.Context, types.DeleteUploadsOptions) error {
panic("unexpected invocation of MockStore.DeleteUploads")
},
},
DeleteUploadsStuckUploadingFunc: &StoreDeleteUploadsStuckUploadingFunc{
defaultHook: func(context.Context, time.Time) (int, error) {
panic("unexpected invocation of MockStore.DeleteUploadsStuckUploading")
@ -760,6 +773,9 @@ func NewMockStoreFrom(i store.Store) *MockStore {
DeleteUploadByIDFunc: &StoreDeleteUploadByIDFunc{
defaultHook: i.DeleteUploadByID,
},
DeleteUploadsFunc: &StoreDeleteUploadsFunc{
defaultHook: i.DeleteUploads,
},
DeleteUploadsStuckUploadingFunc: &StoreDeleteUploadsStuckUploadingFunc{
defaultHook: i.DeleteUploadsStuckUploading,
},
@ -1570,6 +1586,110 @@ func (c StoreDeleteUploadByIDFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// StoreDeleteUploadsFunc describes the behavior when the DeleteUploads
// method of the parent MockStore instance is invoked.
type StoreDeleteUploadsFunc struct {
defaultHook func(context.Context, types.DeleteUploadsOptions) error
hooks []func(context.Context, types.DeleteUploadsOptions) error
history []StoreDeleteUploadsFuncCall
mutex sync.Mutex
}
// DeleteUploads delegates to the next hook function in the queue and stores
// the parameter and result values of this invocation.
func (m *MockStore) DeleteUploads(v0 context.Context, v1 types.DeleteUploadsOptions) error {
r0 := m.DeleteUploadsFunc.nextHook()(v0, v1)
m.DeleteUploadsFunc.appendCall(StoreDeleteUploadsFuncCall{v0, v1, r0})
return r0
}
// SetDefaultHook sets function that is called when the DeleteUploads method
// of the parent MockStore instance is invoked and the hook queue is empty.
func (f *StoreDeleteUploadsFunc) SetDefaultHook(hook func(context.Context, types.DeleteUploadsOptions) error) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// DeleteUploads method of the parent MockStore instance invokes the hook at
// the front of the queue and discards it. After the queue is empty, the
// default hook function is invoked for any future action.
func (f *StoreDeleteUploadsFunc) PushHook(hook func(context.Context, types.DeleteUploadsOptions) error) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *StoreDeleteUploadsFunc) SetDefaultReturn(r0 error) {
f.SetDefaultHook(func(context.Context, types.DeleteUploadsOptions) error {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *StoreDeleteUploadsFunc) PushReturn(r0 error) {
f.PushHook(func(context.Context, types.DeleteUploadsOptions) error {
return r0
})
}
func (f *StoreDeleteUploadsFunc) nextHook() func(context.Context, types.DeleteUploadsOptions) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *StoreDeleteUploadsFunc) appendCall(r0 StoreDeleteUploadsFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of StoreDeleteUploadsFuncCall objects
// describing the invocations of this function.
func (f *StoreDeleteUploadsFunc) History() []StoreDeleteUploadsFuncCall {
f.mutex.Lock()
history := make([]StoreDeleteUploadsFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// StoreDeleteUploadsFuncCall is an object that describes an invocation of
// method DeleteUploads on an instance of MockStore.
type StoreDeleteUploadsFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 types.DeleteUploadsOptions
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c StoreDeleteUploadsFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c StoreDeleteUploadsFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// StoreDeleteUploadsStuckUploadingFunc describes the behavior when the
// DeleteUploadsStuckUploading method of the parent MockStore instance is
// invoked.

View File

@ -450,6 +450,13 @@ func (s *Service) DeleteUploadByID(ctx context.Context, id int) (_ bool, err err
return s.store.DeleteUploadByID(ctx, id)
}
func (s *Service) DeleteUploads(ctx context.Context, opts types.DeleteUploadsOptions) (err error) {
ctx, _, endObservation := s.operations.deleteUploadByID.With(ctx, &err, observation.Args{})
defer endObservation(1, observation.Args{})
return s.store.DeleteUploads(ctx, opts)
}
// numAncestors is the number of ancestors to query from gitserver when trying to find the closest
// ancestor we have data for. Setting this value too low (relative to a repository's commit rate)
// will cause requests for an unknown commit return too few results; setting this value too high

View File

@ -40,6 +40,12 @@ type GetUploadsOptions struct {
InCommitGraph bool
}
type DeleteUploadsOptions struct {
State string
Term string
VisibleAtTip bool
}
type DependencyReferenceCountUpdateType int
const (

View File

@ -18,6 +18,7 @@ type UploadService interface {
GetUploads(ctx context.Context, opts types.GetUploadsOptions) (uploads []types.Upload, totalCount int, err error)
GetUploadsByIDs(ctx context.Context, ids ...int) (_ []types.Upload, err error)
DeleteUploadByID(ctx context.Context, id int) (_ bool, err error)
DeleteUploads(ctx context.Context, opts types.DeleteUploadsOptions) (err error)
}
type AutoIndexingService interface {

View File

@ -878,6 +878,9 @@ type MockUploadService struct {
// DeleteUploadByIDFunc is an instance of a mock function object
// controlling the behavior of the method DeleteUploadByID.
DeleteUploadByIDFunc *UploadServiceDeleteUploadByIDFunc
// DeleteUploadsFunc is an instance of a mock function object
// controlling the behavior of the method DeleteUploads.
DeleteUploadsFunc *UploadServiceDeleteUploadsFunc
// GetAuditLogsForUploadFunc is an instance of a mock function object
// controlling the behavior of the method GetAuditLogsForUpload.
GetAuditLogsForUploadFunc *UploadServiceGetAuditLogsForUploadFunc
@ -908,6 +911,11 @@ func NewMockUploadService() *MockUploadService {
return
},
},
DeleteUploadsFunc: &UploadServiceDeleteUploadsFunc{
defaultHook: func(context.Context, types.DeleteUploadsOptions) (r0 error) {
return
},
},
GetAuditLogsForUploadFunc: &UploadServiceGetAuditLogsForUploadFunc{
defaultHook: func(context.Context, int) (r0 []types.UploadLog, r1 error) {
return
@ -950,6 +958,11 @@ func NewStrictMockUploadService() *MockUploadService {
panic("unexpected invocation of MockUploadService.DeleteUploadByID")
},
},
DeleteUploadsFunc: &UploadServiceDeleteUploadsFunc{
defaultHook: func(context.Context, types.DeleteUploadsOptions) error {
panic("unexpected invocation of MockUploadService.DeleteUploads")
},
},
GetAuditLogsForUploadFunc: &UploadServiceGetAuditLogsForUploadFunc{
defaultHook: func(context.Context, int) ([]types.UploadLog, error) {
panic("unexpected invocation of MockUploadService.GetAuditLogsForUpload")
@ -991,6 +1004,9 @@ func NewMockUploadServiceFrom(i UploadService) *MockUploadService {
DeleteUploadByIDFunc: &UploadServiceDeleteUploadByIDFunc{
defaultHook: i.DeleteUploadByID,
},
DeleteUploadsFunc: &UploadServiceDeleteUploadsFunc{
defaultHook: i.DeleteUploads,
},
GetAuditLogsForUploadFunc: &UploadServiceGetAuditLogsForUploadFunc{
defaultHook: i.GetAuditLogsForUpload,
},
@ -1122,6 +1138,111 @@ func (c UploadServiceDeleteUploadByIDFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// UploadServiceDeleteUploadsFunc describes the behavior when the
// DeleteUploads method of the parent MockUploadService instance is invoked.
type UploadServiceDeleteUploadsFunc struct {
defaultHook func(context.Context, types.DeleteUploadsOptions) error
hooks []func(context.Context, types.DeleteUploadsOptions) error
history []UploadServiceDeleteUploadsFuncCall
mutex sync.Mutex
}
// DeleteUploads delegates to the next hook function in the queue and stores
// the parameter and result values of this invocation.
func (m *MockUploadService) DeleteUploads(v0 context.Context, v1 types.DeleteUploadsOptions) error {
r0 := m.DeleteUploadsFunc.nextHook()(v0, v1)
m.DeleteUploadsFunc.appendCall(UploadServiceDeleteUploadsFuncCall{v0, v1, r0})
return r0
}
// SetDefaultHook sets function that is called when the DeleteUploads method
// of the parent MockUploadService instance is invoked and the hook queue is
// empty.
func (f *UploadServiceDeleteUploadsFunc) SetDefaultHook(hook func(context.Context, types.DeleteUploadsOptions) error) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// DeleteUploads method of the parent MockUploadService instance invokes the
// hook at the front of the queue and discards it. After the queue is empty,
// the default hook function is invoked for any future action.
func (f *UploadServiceDeleteUploadsFunc) PushHook(hook func(context.Context, types.DeleteUploadsOptions) error) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *UploadServiceDeleteUploadsFunc) SetDefaultReturn(r0 error) {
f.SetDefaultHook(func(context.Context, types.DeleteUploadsOptions) error {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *UploadServiceDeleteUploadsFunc) PushReturn(r0 error) {
f.PushHook(func(context.Context, types.DeleteUploadsOptions) error {
return r0
})
}
func (f *UploadServiceDeleteUploadsFunc) nextHook() func(context.Context, types.DeleteUploadsOptions) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *UploadServiceDeleteUploadsFunc) appendCall(r0 UploadServiceDeleteUploadsFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of UploadServiceDeleteUploadsFuncCall objects
// describing the invocations of this function.
func (f *UploadServiceDeleteUploadsFunc) History() []UploadServiceDeleteUploadsFuncCall {
f.mutex.Lock()
history := make([]UploadServiceDeleteUploadsFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// UploadServiceDeleteUploadsFuncCall is an object that describes an
// invocation of method DeleteUploads on an instance of MockUploadService.
type UploadServiceDeleteUploadsFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 types.DeleteUploadsOptions
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c UploadServiceDeleteUploadsFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c UploadServiceDeleteUploadsFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// UploadServiceGetAuditLogsForUploadFunc describes the behavior when the
// GetAuditLogsForUpload method of the parent MockUploadService instance is
// invoked.

View File

@ -12,6 +12,7 @@ type operations struct {
lsifUploadByID *observation.Operation
lsifUploadsByRepo *observation.Operation
deleteLsifUpload *observation.Operation
deleteLsifUploads *observation.Operation
// Commit Graph
commitGraph *observation.Operation
@ -38,6 +39,7 @@ func newOperations(observationContext *observation.Context) *operations {
lsifUploadByID: op("LSIFUploadByID"),
lsifUploadsByRepo: op("LSIFUploadsByRepo"),
deleteLsifUpload: op("DeleteLSIFUpload"),
deleteLsifUploads: op("DeleteLSIFUploads"),
// Commit Graph
commitGraph: op("CommitGraph"),

View File

@ -18,6 +18,7 @@ type RootResolver interface {
LSIFUploads(ctx context.Context, args *LSIFUploadsQueryArgs) (sharedresolvers.LSIFUploadConnectionResolver, error)
LSIFUploadsByRepo(ctx context.Context, args *LSIFRepositoryUploadsQueryArgs) (sharedresolvers.LSIFUploadConnectionResolver, error)
DeleteLSIFUpload(ctx context.Context, args *struct{ ID graphql.ID }) (*sharedresolvers.EmptyResponse, error)
DeleteLSIFUploads(ctx context.Context, args *DeleteLSIFUploadsArgs) (*sharedresolvers.EmptyResponse, error)
}
type rootResolver struct {
@ -96,6 +97,13 @@ type LSIFRepositoryUploadsQueryArgs struct {
RepositoryID graphql.ID
}
type DeleteLSIFUploadsArgs struct {
Query *string
State *string
IsLatestForRepo *bool
Repository *graphql.ID
}
// 🚨 SECURITY: dbstore layer handles authz for GetUploads
func (r *rootResolver) LSIFUploads(ctx context.Context, args *LSIFUploadsQueryArgs) (_ sharedresolvers.LSIFUploadConnectionResolver, err error) {
// Delegate behavior to LSIFUploadsByRepo with no specified repository identifier
@ -145,3 +153,23 @@ func (r *rootResolver) DeleteLSIFUpload(ctx context.Context, args *struct{ ID gr
return &sharedresolvers.EmptyResponse{}, nil
}
// 🚨 SECURITY: Only site admins may modify code intelligence upload data
func (r *rootResolver) DeleteLSIFUploads(ctx context.Context, args *DeleteLSIFUploadsArgs) (_ *sharedresolvers.EmptyResponse, err error) {
ctx, _, endObservation := r.operations.deleteLsifUploads.With(ctx, &err, observation.Args{})
endObservation.OnCancel(ctx, 1, observation.Args{})
if err := backend.CheckCurrentUserIsSiteAdmin(ctx, r.autoindexSvc.GetUnsafeDB()); err != nil {
return nil, err
}
opts, err := makeDeleteUploadsOptions(args)
if err != nil {
return nil, err
}
if err := r.uploadSvc.DeleteUploads(ctx, opts); err != nil {
return nil, err
}
return &sharedresolvers.EmptyResponse{}, nil
}

View File

@ -120,6 +120,26 @@ func makeGetUploadsOptions(args *LSIFRepositoryUploadsQueryArgs) (types.GetUploa
}, nil
}
// makeDeleteUploadsOptions translates the given GraphQL arguments into options defined by the
// store.DeleteUploads operations.
func makeDeleteUploadsOptions(args *DeleteLSIFUploadsArgs) (types.DeleteUploadsOptions, error) {
var repository int
if args.Repository != nil {
var err error
repository, err = resolveRepositoryID(*args.Repository)
if err != nil {
return types.DeleteUploadsOptions{}, err
}
}
return types.DeleteUploadsOptions{
State: strings.ToLower(derefString(args.State, "")),
Term: derefString(args.Query, ""),
VisibleAtTip: derefBool(args.IsLatestForRepo, false),
RepositoryID: repository,
}, nil
}
// resolveRepositoryByID gets a repository's internal identifier from a GraphQL identifier.
func resolveRepositoryID(id graphql.ID) (int, error) {
if id == "" {