Skip to content

Commit 4fe3835

Browse files
authored
feat(cloud_hub): Add deployment to fly.io (#310)
Adds a deployment engine using Fly.io to Celest Cloud.
1 parent de67dbf commit 4fe3835

24 files changed

+11052
-228
lines changed

apps/cli/lib/src/codegen/api/dockerfile_generator.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'package:mustache_template/mustache_template.dart';
77
final class DockerfileGenerator {
88
DockerfileGenerator({required this.project});
99

10-
final ast.Project project;
10+
final ast.ResolvedProject project;
1111

1212
static final Template _dartTemplate = Template(r'''
1313
# syntax=docker/dockerfile:1

apps/cli/lib/src/frontend/celest_frontend.dart

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import 'package:celest_cli/src/repositories/project_repository.dart';
3030
import 'package:celest_cli/src/utils/json.dart';
3131
import 'package:celest_cli/src/utils/recase.dart';
3232
import 'package:celest_cloud/src/proto.dart' as pb;
33-
import 'package:celest_core/_internal.dart';
3433
import 'package:dcli/dcli.dart' as dcli;
3534
import 'package:logging/logging.dart';
3635
import 'package:mason_logger/mason_logger.dart' show Progress;
@@ -555,7 +554,6 @@ final class CelestFrontend {
555554
);
556555
try {
557556
await _writeProjectOutputs(
558-
project: project,
559557
resolvedProject: resolvedProject,
560558
environmentId: environmentId,
561559
);
@@ -653,8 +651,7 @@ final class CelestFrontend {
653651
iteration++;
654652
});
655653
final (deployedProject, baseUri) = await _deployProject(
656-
projectId: projectId,
657-
environmentId: environment.projectEnvironmentId,
654+
environmentName: environment.name,
658655
resolvedProject: resolvedProject,
659656
);
660657
await _generateClientCode(
@@ -745,7 +742,6 @@ final class CelestFrontend {
745742
});
746743

747744
Future<void> _writeProjectOutputs({
748-
required Project project,
749745
required ResolvedProject resolvedProject,
750746
required String environmentId,
751747
}) async {
@@ -788,7 +784,7 @@ final class CelestFrontend {
788784
}
789785
}
790786

791-
final dockerfile = DockerfileGenerator(project: project);
787+
final dockerfile = DockerfileGenerator(project: resolvedProject);
792788
await buildOutputs
793789
.childFile('Dockerfile')
794790
.writeAsString(dockerfile.generate());
@@ -887,12 +883,10 @@ final class CelestFrontend {
887883
}
888884

889885
Future<(ast.ResolvedProject, Uri)> _deployProject({
890-
required String projectId,
891-
required String environmentId,
886+
required String environmentName,
892887
required ast.ResolvedProject resolvedProject,
893888
}) =>
894889
performance.trace('CelestFrontend', 'deployProject', () async {
895-
final deploymentId = Uuid.v7().hexValue;
896890
try {
897891
final entrypointCompiler = EntrypointCompiler(
898892
logger: logger,
@@ -905,7 +899,7 @@ final class CelestFrontend {
905899
entrypointPath: projectPaths.localApiEntrypoint,
906900
);
907901
final operation = cloud.projects.environments.deploy(
908-
'projects/$projectId/environments/$environmentId',
902+
environmentName,
909903
// HACK(dnys1): celest_ast and celest_cloud don't share types.
910904
resolvedProject: pb.ResolvedProject.fromBuffer(
911905
resolvedProject.toProto().writeToBuffer(),
@@ -918,7 +912,6 @@ final class CelestFrontend {
918912
inline: kernel.outputDill,
919913
),
920914
],
921-
requestId: deploymentId,
922915
);
923916
final waiter = CloudCliOperation(
924917
operation,
@@ -945,24 +938,14 @@ final class CelestFrontend {
945938
);
946939
} on Exception catch (e, st) {
947940
if (e case CancellationException() || CliException()) {
948-
analytics.capture(
949-
'cancel_deployment',
950-
properties: {
951-
'deployment_id': deploymentId,
952-
'project_id': projectId,
953-
'environment_id': environmentId,
954-
},
955-
);
956941
rethrow;
957942
}
958943
Error.throwWithStackTrace(
959944
CliException(
960945
'Failed to deploy project. Please contact the Celest team and '
961-
'reference deployment ID: $deploymentId',
946+
'reference environment: $environmentName',
962947
additionalContext: {
963-
'deploymentId': deploymentId,
964-
'project_id': projectId,
965-
'environment_id': environmentId,
948+
'environment_name': environmentName,
966949
'error': '$e',
967950
},
968951
),

apps/cli/lib/src/project/project_linker.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,11 @@ final class ProjectLinker extends AstVisitorWithArg<Node?, AstNode> {
7272
required Map<String, String> configValues,
7373
required String environmentId,
7474
this.driftSchemas = const {},
75-
}) : configValues = {...configValues, 'CELEST_ENVIRONMENT': environmentId};
75+
}) : configValues = {...configValues, 'CELEST_ENVIRONMENT': environmentId},
76+
_resolvedProject = ResolvedProjectBuilder()
77+
..environmentId = environmentId;
7678

77-
final _resolvedProject = ResolvedProjectBuilder();
79+
final ResolvedProjectBuilder _resolvedProject;
7880
late final ResolvedProject resolvedProject = run(() {
7981
final celestConfigValues = configValues.keys.toSet().difference(
8082
_seenConfigValues,

apps/cli/lib/src/repositories/project_repository.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ final class ProjectRepository {
1919
return null;
2020
}
2121
final cloudPrj = await _cloud.projects.get(
22-
'${organization.name}projects/$projectIdOrAlias',
22+
'${organization.name}/projects/$projectIdOrAlias',
2323
);
2424
return cloudPrj;
2525
} on Object catch (e, st) {

packages/celest/example/celest/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33

44
# Celest
55
**/.env
6+
build/

packages/celest_cloud/lib/src/cloud/cloud.dart

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import 'package:celest_cloud/src/cloud/projects/projects.dart';
1111
import 'package:celest_cloud/src/cloud/subscriptions/subscriptions.dart';
1212
import 'package:celest_cloud/src/cloud/users/users.dart';
1313
import 'package:celest_cloud/src/proto.dart';
14+
import 'package:celest_cloud/src/proto/google/protobuf/duration.pb.dart' as pb;
15+
import 'package:celest_cloud/src/proto/google/protobuf/empty.pb.dart' as pb;
16+
import 'package:celest_cloud/src/proto/google/protobuf/field_mask.pb.dart'
17+
as pb;
18+
import 'package:celest_cloud/src/proto/google/protobuf/struct.pb.dart' as pb;
19+
import 'package:celest_cloud/src/proto/google/protobuf/timestamp.pb.dart' as pb;
20+
import 'package:celest_cloud/src/proto/google/protobuf/wrappers.pb.dart' as pb;
21+
import 'package:celest_cloud/src/proto/google/rpc/status.pb.dart' as pb;
1422
import 'package:celest_core/_internal.dart';
1523
import 'package:http/http.dart' as http;
1624
import 'package:logging/logging.dart';
@@ -117,11 +125,32 @@ class CelestCloud {
117125
: ClientType.CLIENT_TYPE_UNSPECIFIED;
118126

119127
static final typeRegistry = TypeRegistry([
120-
Empty(),
128+
// Cloud
121129
OperationMetadata(),
130+
Operation(),
122131
Organization(),
123132
Project(),
124133
ProjectEnvironment(),
134+
DeployProjectEnvironmentResponse(),
135+
136+
// RPC
137+
pb.Status(),
138+
139+
// Well-known types
140+
pb.BoolValue(),
141+
pb.StringValue(),
142+
pb.Int32Value(),
143+
pb.Int64Value(),
144+
pb.FloatValue(),
145+
pb.DoubleValue(),
146+
pb.Timestamp(),
147+
pb.BytesValue(),
148+
pb.Duration(),
149+
pb.FieldMask(),
150+
pb.Empty(),
151+
pb.Struct(),
152+
pb.ListValue(),
153+
pb.Value(),
125154
]);
126155

127156
final Uri? _baseUri;

packages/celest_cloud/lib/src/cloud/project_environments/project_environments_protocol.http.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class ProjectEnvironmentsProtocolHttp
2828
queryParameters: {
2929
if (request.hasParent()) 'parent': request.parent,
3030
if (request.hasProjectEnvironmentId())
31-
'project_environment_id': request.projectEnvironmentId,
31+
'projectEnvironmentId': request.projectEnvironmentId,
3232
if (request.hasValidateOnly())
3333
'validateOnly': request.validateOnly.toString(),
3434
},

services/celest_cloud_hub/analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ analyzer:
66
implementation_imports: ignore
77
exclude:
88
- lib/src/proto/**
9+
- lib/src/**/*.g.dart

services/celest_cloud_hub/bin/cloud_hub.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import 'package:logging/logging.dart';
2626
import 'package:stack_trace/stack_trace.dart';
2727

2828
Future<void> main() async {
29-
context.logger.level = Level.ALL;
29+
context.logger.level = Level.INFO;
3030
context.logger.onRecord.listen((record) {
3131
print('${record.level.name}: ${record.time}: ${record.message}');
3232
if (record.error != null) {
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import 'dart:io';
2+
3+
import 'package:celest_ast/celest_ast.dart' as ast;
4+
import 'package:celest_cli/src/sdk/sdk_finder.dart';
5+
import 'package:celest_cloud_hub/src/database/cloud_hub_database.dart';
6+
import 'package:celest_cloud_hub/src/deploy/fly/fly_deployment_engine.dart';
7+
import 'package:celest_cloud_hub/src/services/service_mixin.dart';
8+
import 'package:file/local.dart';
9+
import 'package:http/http.dart' as http;
10+
import 'package:logging/logging.dart';
11+
import 'package:process/process.dart';
12+
import 'package:pub_semver/pub_semver.dart';
13+
14+
const fileSystem = LocalFileSystem();
15+
const processManager = LocalProcessManager();
16+
17+
Future<void> main() async {
18+
Logger.root.level = Level.ALL;
19+
Logger.root.onRecord.listen((record) {
20+
print(
21+
'[${record.loggerName.split('.').last}] ${record.level}: ${record.message}',
22+
);
23+
});
24+
25+
const sdkFinder = DartSdkFinder();
26+
final sdk = (await sdkFinder.findSdk()).sdk;
27+
28+
const helloWorld = r'''
29+
import 'dart:io';
30+
31+
Future<void> main() async {
32+
final port = int.parse(Platform.environment['PORT'] ?? '8080');
33+
final server = await HttpServer.bind(InternetAddress.anyIPv4, port);
34+
print('Listening on http://localhost:$port');
35+
await for (final request in server) {
36+
request.response.write('Hello, world!');
37+
await request.response.close();
38+
}
39+
}
40+
''';
41+
final tmpDir = fileSystem.systemTempDirectory.createTempSync('celest_').path;
42+
await fileSystem
43+
.directory(tmpDir)
44+
.childFile('hello_world.dart')
45+
.writeAsString(helloWorld);
46+
47+
print('Compiling server');
48+
final res = await processManager.run([
49+
sdk.dartAotRuntime,
50+
sdk.frontendServerAotSnapshot,
51+
'--sdk-root',
52+
sdk.sdkPath,
53+
'--platform',
54+
sdk.vmPlatformProductDill,
55+
'--aot',
56+
'--tfa',
57+
'--no-support-mirrors',
58+
'--link-platform',
59+
'--target=vm',
60+
'-Ddart.vm.product=true',
61+
'--output-dill=$tmpDir/celest.aot.dill',
62+
'$tmpDir/hello_world.dart',
63+
]);
64+
if (res.exitCode != 0) {
65+
throw Exception('Failed to compile hello_world.dart:\n${res.stderr}');
66+
}
67+
final bytes = await fileSystem.file('$tmpDir/celest.aot.dill').readAsBytes();
68+
69+
final db = CloudHubDatabase.memory();
70+
71+
print('Creating organization');
72+
final orgId = typeId('org');
73+
await db.organizationsDrift.createOrganization(
74+
id: orgId,
75+
organizationId: 'my-org',
76+
state: 'ACTIVE',
77+
displayName: 'My Organization',
78+
);
79+
80+
print('Creating project');
81+
final projectId = typeId('prj');
82+
await db.projectsDrift.createProject(
83+
id: projectId,
84+
parentType: 'Celest::Organization',
85+
parentId: orgId,
86+
projectId: 'my-project',
87+
state: 'ACTIVE',
88+
regions: '',
89+
);
90+
91+
print('Creating environment');
92+
final environmentId = typeId('env');
93+
final environment =
94+
(await db.projectEnvironmentsDrift.createProjectEnvironment(
95+
id: environmentId,
96+
parentType: 'Celest::Project',
97+
parentId: projectId,
98+
projectEnvironmentId: 'production',
99+
state: 'CREATING',
100+
)).first;
101+
final flyApiToken = Platform.environment['FLY_API_TOKEN']!;
102+
final deploymentEngine = FlyDeploymentEngine(
103+
db: db,
104+
flyApiToken: flyApiToken,
105+
projectAst: ast.ResolvedProject(
106+
projectId: 'my-project',
107+
environmentId: 'production',
108+
sdkConfig: ast.SdkConfiguration(
109+
celest: Version(1, 0, 9),
110+
dart: ast.Sdk(type: ast.SdkType.dart, version: sdk.version),
111+
),
112+
),
113+
kernelAsset: bytes,
114+
environment: environment,
115+
);
116+
117+
print('Deploying');
118+
final state = await deploymentEngine.deploy();
119+
deploymentEngine.close();
120+
121+
final uri = Uri.parse('https://${state.domainName}');
122+
final response = await http.get(uri);
123+
if (response.statusCode != 200) {
124+
throw http.ClientException(
125+
'Bad response: ${response.statusCode} ${response.body}',
126+
uri,
127+
);
128+
}
129+
print(response.body);
130+
}

0 commit comments

Comments
 (0)