blob: 6a7b9cd87863068d4f18a8039bf615c9ca7da3bd [file] [log] [blame]
// Copyright 2020 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:gcloud/db.dart';
import 'package:github/github.dart';
import 'package:meta/meta.dart';
import '../model/appengine/commit.dart';
import '../model/appengine/key_helper.dart';
import '../model/appengine/task.dart';
import '../model/ci_yaml/ci_yaml.dart';
import '../model/ci_yaml/target.dart';
import '../model/google/token_info.dart';
import '../request_handling/api_request_handler.dart';
import '../request_handling/body.dart';
import '../request_handling/exceptions.dart';
import '../service/config.dart';
import '../service/datastore.dart';
import '../service/logging.dart';
import '../service/luci_build_service.dart';
import '../service/scheduler.dart';
/// Reruns a postsubmit LUCI build.
///
/// Expects either [taskKeyParam] or a set of params that give enough detail to lookup a task in datastore.
@immutable
class ResetProdTask extends ApiRequestHandler<Body> {
const ResetProdTask({
required super.config,
required super.authenticationProvider,
required this.luciBuildService,
required this.scheduler,
@visibleForTesting DatastoreServiceProvider? datastoreProvider,
}) : datastoreProvider = datastoreProvider ?? DatastoreService.defaultProvider;
final DatastoreServiceProvider datastoreProvider;
final LuciBuildService luciBuildService;
final Scheduler scheduler;
static const String branchParam = 'Branch';
static const String taskKeyParam = 'Key';
static const String ownerParam = 'Owner';
static const String repoParam = 'Repo';
static const String commitShaParam = 'Commit';
static const String builderParam = 'Builder';
@override
Future<Body> post() async {
final DatastoreService datastore = datastoreProvider(config.db);
final String? encodedKey = requestData![taskKeyParam] as String?;
String? gitBranch = requestData![branchParam] as String?;
final String owner = requestData![ownerParam] as String? ?? 'flutter';
final String? repo = requestData![repoParam] as String?;
final String? sha = requestData![commitShaParam] as String?;
final TokenInfo token = await tokenInfo(request!);
final String? taskName = requestData![builderParam] as String?;
RepositorySlug? slug;
if (encodedKey != null && encodedKey.isNotEmpty) {
// Check params required for dashboard.
checkRequiredParameters(<String>[taskKeyParam]);
} else {
// Checks params required when this API is called with curl.
checkRequiredParameters(<String>[commitShaParam, builderParam, repoParam]);
slug = RepositorySlug(owner, repo!);
gitBranch ??= Config.defaultBranch(slug);
}
final Task task = await _getTaskFromNamedParams(
datastore: datastore,
encodedKey: encodedKey,
gitBranch: gitBranch,
name: taskName,
sha: sha,
slug: slug,
);
final Commit commit = await _getCommitFromTask(datastore, task);
final CiYaml ciYaml = await scheduler.getCiYaml(commit);
final Target target = ciYaml.postsubmitTargets.singleWhere((Target target) => target.value.name == task.name);
final Map<String, List<String>> tags = <String, List<String>>{
'triggered_by': <String>[token.email!],
'trigger_type': <String>['manual'],
};
final bool isRerunning = await luciBuildService.checkRerunBuilder(
commit: commit,
task: task,
target: target,
datastore: datastore,
tags: tags,
ignoreChecks: true,
);
if (isRerunning == false) {
throw InternalServerError('Failed to rerun ${task.name}');
}
return Body.empty;
}
/// Retrieve [Task] from [DatastoreService] from either an encoded key or commit + task name info.
///
/// If [encodedKey] is passed, [KeyHelper] will decode it directly and return the associated entity.
///
/// Otherwise, [name], [gitBranch], [sha], and [slug] must be passed to find the [Task].
Future<Task> _getTaskFromNamedParams({
required DatastoreService datastore,
String? encodedKey,
String? gitBranch,
String? name,
String? sha,
RepositorySlug? slug,
}) async {
if (encodedKey != null && encodedKey.isNotEmpty) {
final Key<int> key = config.keyHelper.decode(encodedKey) as Key<int>;
return datastore.lookupByValue<Task>(key);
}
final Key<String> commitKey = await _constructCommitKey(
datastore: datastore,
gitBranch: gitBranch!,
sha: sha!,
slug: slug!,
);
final Query<Task> query = datastore.db.query<Task>(ancestorKey: commitKey);
final List<Task> initialTasks = await query.run().toList();
log.fine('Found ${initialTasks.length} tasks for commit');
final List<Task> tasks = <Task>[];
log.fine('Searching for task with name=$name');
for (Task task in initialTasks) {
if (task.name == name) {
tasks.add(task);
}
}
if (tasks.length != 1) {
log.severe('Found ${tasks.length} entries for builder $name');
throw InternalServerError('Expected to find 1 task for $name, but found ${tasks.length}');
}
return tasks.single;
}
/// Construct the Datastore key for [Commit] that is the ancestor to this [Task].
///
/// Throws [BadRequestException] if the given git branch does not exist in [CocoonConfig].
Future<Key<String>> _constructCommitKey({
required DatastoreService datastore,
required String gitBranch,
required String sha,
required RepositorySlug slug,
}) async {
gitBranch = gitBranch.trim();
sha = sha.trim();
final String id = '${slug.fullName}/$gitBranch/$sha';
final Key<String> commitKey = datastore.db.emptyKey.append<String>(Commit, id: id);
log.fine('Constructed commit key=$id');
// Return the official key from Datastore for task lookups.
final Commit commit = await datastore.lookupByValue<Commit>(commitKey);
return commit.key;
}
/// Returns the [Commit] associated with [Task].
Future<Commit> _getCommitFromTask(DatastoreService datastore, Task task) async {
return (await datastore.lookupByKey<Commit>(<Key<dynamic>>[task.parentKey!])).single!;
}
}