Skip to content

Commit

Permalink
feat: add tekartik_gcr_build
Browse files Browse the repository at this point in the history
  • Loading branch information
alextekartik committed Oct 10, 2024
1 parent 3059575 commit 0aa2192
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/gcr_build/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/

# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock
14 changes: 14 additions & 0 deletions packages/gcr_build/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Google cloud run support

Add helper to build and deploy to Google Cloud Run

## Setup

```yaml
dependencies:
tekartik_gcr_build:
git:
url: https://github.com/tekartik/app_build.dart
ref: dart3a
path: packages/gcr_build
```
30 changes: 30 additions & 0 deletions packages/gcr_build/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.

include: package:tekartik_lints/package.yaml

# Uncomment the following section to specify additional rules.

# linter:
# rules:
# - camel_case_types

# analyzer:
# exclude:
# - path/to/excluded/files/**

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints

# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
6 changes: 6 additions & 0 deletions packages/gcr_build/lib/gcr.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
library;

export 'src/docker.dart' show dockerKillAll;
export 'src/gcr_artifact_repository.dart' show GcrArtifactRepository;
export 'src/gcr_project.dart' show GcrProject, GcrProjectExt;
export 'src/gcr_project_options.dart' show GcrProjectOptions, gcrRegionBelgium;
25 changes: 25 additions & 0 deletions packages/gcr_build/lib/src/docker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:dev_build/shell.dart';
import 'package:process_run/stdio.dart';

/// Kill all running instances
Future<void> dockerKillAll() async {
var processIds = (await run(r'''
docker ps -q
''')).outLines.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
if (processIds.isEmpty) {
stdout.writeln('No processIds found');
return;
}
stdout.writeln('processIds: $processIds');
await run('''
docker kill ${processIds.join(' ')}
''');
}

/// Get any running process ids
Future<List<String>> dockerComposeGetRunningProcessIds() async {
var processIds = (await run(r'''
docker compose ps -q
''')).outLines.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
return processIds;
}
35 changes: 35 additions & 0 deletions packages/gcr_build/lib/src/gcr_artifact_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// {
// 'cleanupPolicyDryRun': true,
// 'createTime': '2022-11-13T15:57:13.330908Z',
// 'description':
// 'This repository is created and used by Cloud Functions for storing function docker images.',
// 'format': 'DOCKER',
// 'labels': {'goog-managed-by': 'cloudfunctions'},
// 'mode': 'STANDARD_REPOSITORY',
// 'name': 'projects/my_project/locations/my_region/repositories/my_repo',
// 'satisfiesPzi': true,
// 'sizeBytes': '550708161',
// 'updateTime': '2024-10-04T18:24:41.972377Z'
// };
/// Google cloud artifact repository
class GcrArtifactRepository {
/// Name
final String name;

List<String> get _parts => name.split('/');

/// String repo
String get repository => _parts.last;

/// String region
String get region => _parts[3];

/// String project
String get project => _parts[1];

/// Constructor
GcrArtifactRepository.fromJson(Map map) : name = map['name'] as String;

@override
String toString() => name;
}
160 changes: 160 additions & 0 deletions packages/gcr_build/lib/src/gcr_project.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import 'package:dev_build/shell.dart';
import 'package:process_run/stdio.dart';
import 'package:tekartik_common_build/version_io.dart';
import 'package:tekartik_common_utils/common_utils_import.dart';
import 'package:tekartik_gcr_build/gcr.dart';
import 'package:tekartik_gcr_build/src/docker.dart';

var _debug = false; // devWarning(true);
void _log(Object? message) {
if (_debug) {
// ignore: avoid_print
print(message);
}
}

/// Google cloud project
class GcrProject {
/// If verbose logging is done
final bool verbose;

/// Path to the project
final String path;

/// Options
final GcrProjectOptions options;

/// Constructor
GcrProject({this.path = '.', required this.options, bool? verbose})
: verbose = verbose ?? false;
}

/// Helpers
extension GcrProjectExt on GcrProject {
Shell get _shell =>
Shell(workingDirectory: path, verbose: verbose, commandVerbose: true);

/// Configure docker auth, need after login once per region
Future<void> configureDockerAuth() async {
await _shell.run('''
gcloud auth configure-docker ${options.region}-docker.pkg.dev
''');
}

/// Tag the image before pushing
Future<void> dockerTagImage() async {
await _shell.run('''
docker tag ${options.image} \\
${options.region}-docker.pkg.dev/${options.projectId}/${options.name}/${options.image}
''');
}

/// Rebuild and run
Future<void> buildAndRun() async {
try {
await kill();
} catch (_) {}
if (_debug) {
_log('Before running');
}
try {
await _shell.cloneWithOptions(_shell.options.clone(verbose: true)).run('''
docker compose up --build --pull never
''');
if (_debug) {
_log('After running');
}
} finally {
if (_debug) {
_log('Finally running');
}
}
}

/// Rebuild and run
Future<void> build() async {
await _shell.run('''
docker compose build
''');
}

/// Full build and deploy
Future<void> buildAndDeploy() async {
var futures = [
shellStdioLinesGrouper.runZoned(() async {
await generateVersion();
await build();
await dockerTagImage();
}),
shellStdioLinesGrouper.runZoned(() async {
await configureDockerAuth();
await createArtifactRepository();
})
];
await Future.wait(futures);
await dockerPush();
await deploy();
}

/// List existing artifact repositories
Future<List<GcrArtifactRepository>> listArtifactRepositories() async {
var result = await _shell.run('''
gcloud artifacts repositories list --project=${options.projectId} --format json
''');
var text = result.outText;
var list = jsonDecode(text.trim()) as List;
return list
.map((item) => GcrArtifactRepository.fromJson(item as Map))
.toList();
}

/// Create artifact repository
Future<void> createArtifactRepository({bool? force}) async {
force ??= false;
if (!force) {
var list = await listArtifactRepositories();
if (list.map((e) => e.repository).contains(options.name)) {
stdout.writeln('Artifact repository "${options.name}" already exists');
return;
}
}
await _shell.run('''
gcloud artifacts repositories create ${options.name} --repository-format=docker \\
--location=${options.region} \\
${options.description == null ? '' : '--description ${shellArgument(options.description!)}'} \\
--project=${options.projectId}
''');
}

/// Terminate/kill
Future<void> kill() async {
var processIds = await dockerComposeGetRunningProcessIds();
if (processIds.isNotEmpty) {
await _shell.run('''
docker compose kill
''');
}
}

String get _dockerImage =>
'${options.region}-docker.pkg.dev/${options.projectId}/${options.name}/${options.image}';

/// Push the docker image
Future<void> dockerPush() async {
await _shell.run('''
docker push ${shellArgument(_dockerImage)}
''');
}

/// Deploy the docker image (must be pushed first)
Future<void> deploy() async {
await _shell.cloneWithOptions(_shell.options.clone(verbose: true)).run('''
gcloud run deploy ${options.serviceName} \\
--region ${options.region} \\
--project ${options.projectId} \\
--image ${shellArgument(_dockerImage)} \\
${options.memory == null ? '' : '--memory ${options.memory}'} \\
--allow-unauthenticated
''');
}
}
39 changes: 39 additions & 0 deletions packages/gcr_build/lib/src/gcr_project_options.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// Google cloud region
var gcrRegionBelgium = 'europe-west1';

/// Google cloud project id
class GcrProjectOptions {
/// Google cloud project id
final String projectId;

/// Region
final String region;

/// Artifact (repo) name, unique per project
/// Names may only contain lowercase letters, numbers, and hyphens, and must begin with a letter and end with a letter
final String name;

/// Image name, unique per artifact
/// Must match the image name in the docker-compose file
final String image;

/// Deployed service name (exported by the project as part of the url of the service)
/// The name must use only lowercase alphanumeric characters and dashes, cannot begin or end with a dash, and cannot be longer than 63 characters.
final String serviceName;

/// Description
final String? description;

/// Memory 1G, 2G, 4G, 8G, 16G, 512MB (default)
final String? memory;

/// Constructor
GcrProjectOptions(
{required this.projectId,
required this.region,
required this.name,
required this.image,
this.description,
required this.serviceName,
this.memory});
}
26 changes: 26 additions & 0 deletions packages/gcr_build/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: tekartik_gcr_build
description: Google Cloud Run experiment
version: 1.0.0
publish_to: none

environment:
sdk: ^3.5.0

# Add regular dependencies here.
dependencies:
path:
tekartik_common_utils:
git:
url: https://github.com/tekartik/common_utils.dart
ref: dart3a
dev_build: ">=1.0.1"
process_run: ">=1.2.1"
tekartik_common_build:
git:
url: https://github.com/tekartik/app_build.dart
ref: dart3a
path: packages/common_build

dev_dependencies:
lints: ">=5.0.0"
test: ">=1.24.0"
27 changes: 27 additions & 0 deletions packages/gcr_build/test/gcr_project_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Example showing how to use the JsonCodable macro
import 'package:tekartik_gcr_build/gcr.dart';
import 'package:test/test.dart';

void main() {
test('GcrArtifactRepository', () {
var artifactRepositoryMap = {
'cleanupPolicyDryRun': true,
'createTime': '2022-11-13T15:57:13.330908Z',
'description':
'This repository is created and used by Cloud Functions for storing function docker images.',
'format': 'DOCKER',
'labels': {'goog-managed-by': 'cloudfunctions'},
'mode': 'STANDARD_REPOSITORY',
'name': 'projects/my_project/locations/my_region/repositories/my_repo',
'satisfiesPzi': true,
'sizeBytes': '550708161',
'updateTime': '2024-10-04T18:24:41.972377Z'
};
var repo = GcrArtifactRepository.fromJson(artifactRepositoryMap);
expect(repo.name,
'projects/my_project/locations/my_region/repositories/my_repo');
expect(repo.project, 'my_project');
expect(repo.region, 'my_region');
expect(repo.repository, 'my_repo');
});
}

0 comments on commit 0aa2192

Please sign in to comment.