blob: 70dbc5ef2dec27390b50f850d5d128c54f3288a7 [file] [log] [blame]
// Copyright 2019 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:convert';
import 'dart:typed_data';
import 'package:appengine/appengine.dart';
import 'package:corsac_jwt/corsac_jwt.dart';
import 'package:gcloud/db.dart';
import 'package:gcloud/service_scope.dart' as ss;
import 'package:github/github.dart';
import 'package:googleapis/bigquery/v2.dart' as bigquery;
import 'package:googleapis_auth/auth.dart';
import 'package:graphql/client.dart' hide Cache;
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import '../../cocoon_service.dart';
import '../foundation/providers.dart';
import '../foundation/utils.dart';
import '../model/appengine/key_helper.dart';
import '../model/appengine/service_account_info.dart';
import '../service/access_client_provider.dart';
import '../service/bigquery.dart';
import '../service/github_service.dart';
class Config {
Config(this._db, this._cache) : assert(_db != null);
final DatastoreDB _db;
final CacheService _cache;
/// List of Github presubmit supported repos.
static const Set<String> supportedRepos = <String>{
'engine',
'flutter',
'cocoon',
'packages',
};
@visibleForTesting
static const String configCacheName = 'config';
@visibleForTesting
static const Duration configCacheTtl = Duration(hours: 12);
Logging get loggingService => ss.lookup(#appengine.logging) as Logging;
Future<List<String>> _getFlutterBranches() async {
final Uint8List cacheValue = await _cache.getOrCreate(
configCacheName,
'flutterBranches',
createFn: () => getBranches(
Providers.freshHttpClient, loggingService, twoSecondLinearBackoff),
ttl: configCacheTtl,
);
return String.fromCharCodes(cacheValue).split(',');
}
Future<String> _getSingleValue(String id) async {
final Uint8List cacheValue = await _cache.getOrCreate(
configCacheName,
id,
createFn: () => _getValueFromDatastore(id),
ttl: configCacheTtl,
);
return String.fromCharCodes(cacheValue);
}
Future<Uint8List> _getValueFromDatastore(String id) async {
final CocoonConfig cocoonConfig = CocoonConfig()
..id = id
..parentKey = _db.emptyKey;
final CocoonConfig result =
await _db.lookupValue<CocoonConfig>(cocoonConfig.key);
return Uint8List.fromList(result.value.codeUnits);
}
// GitHub App properties.
Future<String> get githubPrivateKey =>
_getSingleValue('githubapp_private_pem');
Future<String> get githubPublicKey => _getSingleValue('githubapp_public_pem');
Future<String> get githubAppId => _getSingleValue('githubapp_id');
Future<Map<String, dynamic>> get githubAppInstallations async {
final String installations =
await _getSingleValue('githubapp_installations');
return jsonDecode(installations) as Map<String, dynamic>;
}
DatastoreDB get db => _db;
Future<List<String>> get flutterBranches => _getFlutterBranches();
Future<String> get oauthClientId => _getSingleValue('OAuthClientId');
Future<String> get githubOAuthToken => _getSingleValue('GitHubPRToken');
String get nonMasterPullRequestMessage => 'This pull request was opened '
'against a branch other than _master_. Since Flutter pull requests should '
'not normally be opened against branches other than master, I have changed '
'the base to master. If this was intended, you may modify the base back to '
'{{branch}}. See the [Release Process]'
'(https://github.com/flutter/flutter/wiki/Release-process) for information '
'about how other branches get updated.\n\n'
'__Reviewers__: Use caution before merging pull requests to branches other '
'than master, unless this is an intentional hotfix/cherrypick.';
Future<String> get webhookKey => _getSingleValue('WebhookKey');
String get missingTestsPullRequestMessage => 'It looks like this pull '
'request may not have tests. Please make sure to add tests before merging. '
'If you need an exemption to this rule, contact Hixie on the #hackers '
'channel in [Chat](https://github.com/flutter/flutter/wiki/Chat).'
'\n\n'
'__Reviewers__: Read the [Tree Hygiene page]'
'(https://github.com/flutter/flutter/wiki/Tree-hygiene#how-to-review-code) '
'and make sure this patch meets those guidelines before LGTMing.';
String get goldenBreakingChangeMessage =>
'Changes to golden files are considered breaking changes, so consult '
'[Handling Breaking Changes](https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes) '
'to proceed. While there are exceptions to this rule, if this patch modifies '
'an existing golden file, it is probably not an exception. Only new golden '
'file tests, or downstream changes like those from skia updates are '
'considered non-breaking.\n\n'
'For more guidance, visit '
'[Writing a golden file test for `package:flutter`](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter).\n\n'
'__Reviewers__: Read the [Tree Hygiene page](https://github.com/flutter/flutter/wiki/Tree-hygiene#how-to-review-code) '
'and make sure this patch meets those guidelines before LGTMing.';
String get goldenTriageMessage => 'Nice merge! 🎉\n'
'It looks like this PR made changes to golden files. If these changes have '
'not been triaged as a tryjob, be sure to visit '
'[Flutter Gold](https://flutter-gold.skia.org/?query=source_type%3Dflutter) '
'to triage the results when post-submit testing has completed. The status '
'of these tests can be seen on the '
'[Flutter Dashboard](https://flutter-dashboard.appspot.com/build.html).\n'
'Also, be sure to include this change in the [Changelog](https://github.com/flutter/flutter/wiki/Changelog).\n\n'
'For more information about working with golden files, see the wiki page '
'[Writing a Golden File Test for package:flutter/flutter](https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter).';
int get maxTaskRetries => 2;
/// The number of times to retry a LUCI job on infra failures.
int get luciTryInfraFailureRetries => 2;
/// The default number of commit shown in flutter build dashboard.
int get commitNumber => 30;
// TODO(keyonghan): update all existing APIs to use this reference, https://github.com/flutter/flutter/issues/48987.
KeyHelper get keyHelper =>
KeyHelper(applicationContext: context.applicationContext);
String get cqLabelName => 'CQ+1';
String get defaultBranch => 'master';
// Default number of commits to return for benchmark dashboard.
int get maxRecords => 50;
// Repository status context for github status.
String get flutterBuild => 'flutter-build';
// Repository status description for github status.
String get flutterBuildDescription =>
'Flutter build is currently broken. Please do not merge this '
'PR unless it contains a fix to the broken build.';
RepositorySlug get flutterSlug => RepositorySlug('flutter', 'flutter');
String get waitingForTreeToGoGreenLabelName => 'waiting for tree to go green';
Future<ServiceAccountInfo> get deviceLabServiceAccount async {
final String rawValue = await _getSingleValue('DevicelabServiceAccount');
return ServiceAccountInfo.fromJson(
json.decode(rawValue) as Map<String, dynamic>);
}
Future<ServiceAccountCredentials> get taskLogServiceAccount async {
final String rawValue = await _getSingleValue('TaskLogServiceAccount');
return ServiceAccountCredentials.fromJson(json.decode(rawValue));
}
/// The names of autoroller accounts for the repositories.
///
/// These accounts should not need reviews before merging. See
/// https://github.com/flutter/flutter/wiki/Autorollers
Set<String> get rollerAccounts => const <String>{
'skia-flutter-autoroll',
'engine-flutter-autoroll',
};
/// A List of builders for LUCI
List<Map<String, dynamic>> get luciBuilders => <Map<String, String>>[
<String, String>{
'name': 'Linux',
'repo': 'flutter',
'taskName': 'linux_bot',
},
<String, String>{
'name': 'Mac',
'repo': 'flutter',
'taskName': 'mac_bot',
},
<String, String>{
'name': 'Windows',
'repo': 'flutter',
'taskName': 'windows_bot',
},
<String, String>{
'name': 'Linux Coverage',
'repo': 'flutter',
},
<String, String>{
'name': 'Linux Host Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Linux Fuchsia',
'repo': 'engine',
},
<String, String>{
'name': 'Linux Android AOT Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Linux Android Debug Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac Host Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac Android AOT Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac Android Debug Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac iOS Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac iOS Engine Profile',
'repo': 'engine',
},
<String, String>{
'name': 'Mac iOS Engine Release',
'repo': 'engine',
},
<String, String>{
'name': 'Windows Host Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Windows Android AOT Engine',
'repo': 'engine',
}
];
/// A List of try builders for LUCI
List<Map<String, dynamic>> get luciTryBuilders => <Map<String, String>>[
<String, String>{
'name': 'Cocoon',
'repo': 'cocoon',
},
<String, String>{
'name': 'Linux',
'repo': 'flutter',
'taskName': 'linux_bot',
},
<String, String>{
'name': 'Windows',
'repo': 'flutter',
'taskName': 'windows_bot',
},
<String, String>{
'name': 'Linux Host Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Linux Fuchsia',
'repo': 'engine',
},
<String, String>{
'name': 'Linux Android AOT Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Linux Android Debug Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Linux Web Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac Host Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac Android AOT Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac Android Debug Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac Host Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac iOS Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Windows Host Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Windows Android AOT Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Windows Web Engine',
'repo': 'engine',
},
<String, String>{
'name': 'Mac Web Engine',
'repo': 'engine',
},
<String, String>{
'name': 'fuchsia_ctl',
'repo': 'packages',
},
];
Future<String> generateJsonWebToken() async {
final String privateKey = await githubPrivateKey;
final String publicKey = await githubPublicKey;
final JWTBuilder builder = JWTBuilder();
final DateTime now = DateTime.now();
builder
..issuer = await githubAppId
..issuedAt = now
..expiresAt = now.add(const Duration(minutes: 10));
final JWTRsaSha256Signer signer =
JWTRsaSha256Signer(privateKey: privateKey, publicKey: publicKey);
final JWT signedToken = builder.getSignedToken(signer);
return signedToken.toString();
}
Future<String> generateGithubToken(String owner, String repository) async {
final Map<String, dynamic> appInstallations = await githubAppInstallations;
final String appInstallation =
appInstallations['$owner/$repository']['installation_id'] as String;
final String jsonWebToken = await generateJsonWebToken();
final Map<String, String> headers = <String, String>{
'Authorization': 'Bearer $jsonWebToken',
'Accept': 'application/vnd.github.machine-man-preview+json'
};
final http.Response response = await http.post(
'https://api.github.com/app/installations/$appInstallation/access_tokens',
headers: headers);
final Map<String, dynamic> jsonBody =
jsonDecode(response.body) as Map<String, dynamic>;
return jsonBody['token'] as String;
}
Future<GitHub> createGitHubClient(String owner, String repository) async {
final Map<String, dynamic> appInstallations = await githubAppInstallations;
String githubToken;
if (appInstallations.containsKey('$owner/$repository')) {
githubToken = await generateGithubToken(owner, repository);
} else {
githubToken = await githubOAuthToken;
}
return GitHub(auth: Authentication.withToken(githubToken));
}
Future<GraphQLClient> createGitHubGraphQLClient() async {
final HttpLink httpLink = HttpLink(
uri: 'https://api.github.com/graphql',
headers: <String, String>{
'Accept': 'application/vnd.github.antiope-preview+json',
},
);
final String token = await githubOAuthToken;
final AuthLink _authLink = AuthLink(
getToken: () async => 'Bearer $token',
);
final Link link = _authLink.concat(httpLink);
return GraphQLClient(
cache: InMemoryCache(),
link: link,
);
}
Future<GraphQLClient> createCirrusGraphQLClient() async {
final HttpLink httpLink = HttpLink(
uri: 'https://api.cirrus-ci.com/graphql',
);
return GraphQLClient(
cache: InMemoryCache(),
link: httpLink,
);
}
Future<bigquery.TabledataResourceApi> createTabledataResourceApi() async {
final AccessClientProvider accessClientProvider =
AccessClientProvider(await deviceLabServiceAccount);
return await BigqueryService(accessClientProvider).defaultTabledata();
}
Future<GithubService> createGithubService(
String owner, String repository) async {
final GitHub github = await createGitHubClient(owner, repository);
return GithubService(github);
}
bool githubPresubmitSupportedRepo(String repositoryName) {
return supportedRepos.contains(repositoryName);
}
Future<RepositorySlug> repoNameForBuilder(String builderName) async {
final List<Map<String, dynamic>> builders = luciTryBuilders;
final Map<String, dynamic> builderConfig = builders.firstWhere(
(Map<String, dynamic> builder) => builder['name'] == builderName,
orElse: () => <String, String>{'repo': ''},
);
final String repoName = builderConfig['repo'] as String;
// If there is no builder config for the builderName then we
// return null. This is to allow the code calling this method
// to skip changes that depend on builder configurations.
if (repoName.isEmpty) {
return null;
}
return RepositorySlug('flutter', repoName);
}
}
@Kind(name: 'CocoonConfig', idType: IdType.String)
class CocoonConfig extends Model {
@StringProperty(propertyName: 'ParameterValue')
String value;
}
class InvalidConfigurationException implements Exception {
const InvalidConfigurationException(this.id);
final String id;
@override
String toString() => 'Invalid configuration value for $id';
}