blob: cabd97ab6a2e6cb02247059b7cf3c616d0d31a67 [file] [log] [blame]
// Copyright 2016 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 'dart:convert' show json;
import 'package:args/args.dart';
import 'package:http/http.dart';
import 'package:meta/meta.dart';
import 'package:cocoon_agent/src/utils.dart';
class AuthenticatedClient extends BaseClient {
AuthenticatedClient(this._agentId, this._authToken);
final String _agentId;
final String _authToken;
final Client _delegate = new Client();
@override
Future<StreamedResponse> send(BaseRequest request) async {
request.headers['Agent-ID'] = _agentId;
request.headers['Agent-Auth-Token'] = _authToken;
final StreamedResponse resp = await _delegate.send(request);
if (resp.statusCode != 200) {
throw new ClientException(
'AuthenticatedClientError:\n'
' URI: ${request.url}\n'
' HTTP Status: ${resp.statusCode}\n'
' Response body:\n'
'${(await Response.fromStream(resp)).body}',
request.url);
}
return resp;
}
}
/// Contains information about a Cocoon task.
class CocoonTask {
CocoonTask({
@required this.name,
@required this.revision,
@required this.timeoutInMinutes,
this.key,
this.cloudAuthToken,
});
/// Task name as it appears on dashboards and in logs.
final String name;
/// Identifies the task in the database, where the tasks status is stored.
final String key;
/// The Flutter revision (git SHA) this task is expected to run with.
final String revision;
/// Task timeout.
final int timeoutInMinutes;
/// Authentication token that gives the task write access to Google Cloud
/// Storage buckets to upload artifacts, such as screenshots.
final String cloudAuthToken;
}
/// Client to the Coocon backend.
class Agent {
Agent(
{@required this.baseCocoonUrl,
@required this.agentId,
@required this.authToken})
: httpClient = new AuthenticatedClient(agentId, authToken);
final String baseCocoonUrl;
final String agentId;
final String authToken;
Client httpClient;
/// Makes a REST API request to Cocoon.
Future<dynamic> _cocoon(String apiPath, [dynamic jsonData]) async {
String url = '$baseCocoonUrl/api/$apiPath';
Response resp;
if (jsonData != null) {
resp = await httpClient.post(url, body: json.encode(jsonData));
} else {
resp = await httpClient.get(url);
}
return json.decode(resp.body);
}
Future<void> uploadLogChunk(String taskKey, String chunk) async {
if (taskKey == null) return;
String url = '$baseCocoonUrl/api/append-log?ownerKey=${taskKey}';
Response resp = await httpClient.post(url, body: chunk);
if (resp.statusCode != 200) {
throw 'Failed uploading log chunk. Server responded with HTTP status ${resp.statusCode}\n'
'${resp.body}';
}
}
/// Reserves a task in Cocoon backend to be performed by this agent.
///
/// If not tasks are available returns `null`.
Future<CocoonTask> reserveTask() async {
Map<String, dynamic> reservation =
await _cocoon('reserve-task', {'AgentID': agentId})
as Map<String, dynamic>;
if (reservation['TaskEntity'] != null) {
return new CocoonTask(
name: reservation['TaskEntity']['Task']['Name'] as String,
key: reservation['TaskEntity']['Key'] as String,
revision: reservation['ChecklistEntity']['Checklist']['Commit']['Sha']
as String,
timeoutInMinutes:
reservation['TaskEntity']['Task']['TimeoutInMinutes'] as int,
cloudAuthToken: reservation['CloudAuthToken'] as String,
);
}
return null;
}
Future<void> reportSuccess(String taskKey, Map<String, dynamic> resultData,
dynamic benchmarkScoreKeys) async {
var status = <String, dynamic>{
'TaskKey': taskKey,
'NewStatus': 'Succeeded',
};
// Make a copy of resultData because we may alter it during score key
// validation below.
resultData = resultData != null
? new Map<String, dynamic>.from(resultData)
: <String, dynamic>{};
status['ResultData'] = resultData;
var validScoreKeys = <String>[];
if (benchmarkScoreKeys != null) {
for (var rawScoreKey in benchmarkScoreKeys) {
String scoreKey = rawScoreKey as String;
Object score = resultData[scoreKey];
if (score is num) {
// Convert all metrics to double, which provide plenty of precision
// without having to add support for multiple numeric types on the
// backend.
resultData[scoreKey] = score.toDouble();
validScoreKeys.add(scoreKey);
} else {
logger.warning(
'non-numeric score value $score submitted for $scoreKey');
}
}
}
status['BenchmarkScoreKeys'] = validScoreKeys;
await _cocoon('update-task-status', status);
}
Future<void> reportFailure(String taskKey, String reason) async {
await uploadLogChunk(
taskKey, '\n\nTask failed with the following reason:\n$reason\n');
await _cocoon('update-task-status', {
'TaskKey': taskKey,
'NewStatus': 'Failed',
});
}
Future<String> getAuthenticationStatus() async {
return (await _cocoon('get-authentication-status'))['Status'] as String;
}
Future<void> updateHealthStatus(AgentHealth health) async {
await _cocoon('update-agent-health', {
'AgentID': agentId,
'IsHealthy': health.ok,
'HealthDetails': '$health',
});
}
void resetHttpClient() {
if (httpClient != null) {
httpClient.close();
}
httpClient = new AuthenticatedClient(agentId, authToken);
}
void close() {
if (httpClient != null) {
httpClient.close();
}
httpClient = null;
}
}
abstract class Command {
Command(this.name, this.agent);
/// Command name as it appears in the CLI.
final String name;
/// Coocon agent client.
final Agent agent;
Future<Null> run(ArgResults args);
}
/// Overall health of the agent.
class AgentHealth {
/// Check results keyed by parameter.
final Map<String, HealthCheckResult> checks = <String, HealthCheckResult>{};
/// Whether all [checks] succeeded.
bool get ok =>
checks.isNotEmpty &&
checks.values.every((HealthCheckResult r) => r.succeeded);
/// Sets a health check [result] for a given [parameter].
void operator []=(String parameter, HealthCheckResult result) {
if (checks.containsKey(parameter)) {
logger.warning('duplicate health check ${parameter} submitted');
}
checks[parameter] = result;
}
void addAll(Map<String, HealthCheckResult> checks) {
checks.forEach((String p, HealthCheckResult r) {
this[p] = r;
});
}
/// Human-readable printout of the agent's health status.
@override
String toString() {
StringBuffer buf = new StringBuffer();
checks.forEach((String parameter, HealthCheckResult result) {
buf.writeln('$parameter: $result');
});
return buf.toString();
}
}