Resources are the building blocks of stacks in DRFT. This guide explains how to create and use resources for managing any type of declarative resource.
All resources extend the Resource base class:
abstract class Resource {
final String id;
final String type;
final Map<String, dynamic> properties;
final List<String> dependencies;
Resource({
required this.id,
required this.type,
required this.properties,
this.dependencies = const [],
});
void validate();
Map<String, dynamic> toJson();
}Resources should be concrete classes with typed properties:
class VirtualMachine extends Resource {
final String name;
final String image;
final String size;
final String? subnetId;
final List<String>? securityGroupIds;
final Map<String, String>? tags;
VirtualMachine({
required this.name,
required this.image,
required this.size,
this.subnetId,
this.securityGroupIds,
this.tags,
}) : super(
id: 'vm.$name',
type: 'aws.ec2.instance',
properties: {
'name': name,
'image': image,
'size': size,
'subnetId': subnetId,
'securityGroupIds': securityGroupIds,
'tags': tags,
},
);
@override
void validate() {
if (name.isEmpty) {
throw ValidationException('VM name cannot be empty');
}
if (!['small', 'medium', 'large'].contains(size)) {
throw ValidationException('Invalid VM size: $size');
}
}
}Resources use final properties set via constructor parameters, similar to Flutter widgets:
final vm = VirtualMachine(
name: 'web-server',
image: 'ubuntu-22.04',
size: 'medium',
subnetId: subnet.id,
securityGroupIds: [sg1.id, sg2.id],
tags: {
'Environment': 'production',
'Role': 'web-server',
},
);For resources with many optional parameters, use named parameters with sensible defaults:
final database = Database(
name: 'app-db',
engine: 'postgresql',
version: '14',
instanceClass: 'db.t3.medium',
storage: 100,
backupRetention: 7,
multiAz: true,
publiclyAccessible: false,
subnetGroup: dbSubnetGroup.id,
securityGroups: [dbSecurityGroup.id],
tags: {
'Environment': 'production',
'Backup': 'enabled',
},
);class VirtualMachine extends Resource {
// Standard constructor
VirtualMachine({/* ... */});
// Named constructor for common configurations
VirtualMachine.production({
required String name,
required String image,
}) : this(
name: name,
image: image,
size: 'large',
tags: {'Environment': 'production'},
);
VirtualMachine.development({
required String name,
required String image,
}) : this(
name: name,
image: image,
size: 'small',
tags: {'Environment': 'development'},
);
}final network = Network(name: 'main-network');
final subnet = Subnet(
name: 'public-subnet',
networkId: network.id,
cidr: '10.0.1.0/24',
dependsOn: [network.id], // Explicit dependency (if needed)
);DRFT automatically detects dependencies when resources reference each other:
final network = Network(name: 'main-network');
final subnet = Subnet(
name: 'public-subnet',
networkId: network.id, // Implicit dependency detected
);class Resource {
List<String> get implicitDependencies {
final deps = <String>[];
// Scan properties for resource references
for (final value in properties.values) {
if (value is ResourceReference) {
deps.add(value.resourceId);
} else if (value is List) {
for (final item in value) {
if (item is ResourceReference) {
deps.add(item.resourceId);
}
}
}
}
return deps;
}
List<String> get allDependencies {
return [...dependencies, ...implicitDependencies];
}
}class VirtualMachine extends Resource {
final String name;
final String image;
final String size;
final String? subnetId;
final List<String>? securityGroupIds;
final String? userData;
final Map<String, String>? tags;
VirtualMachine({
required this.name,
required this.image,
required this.size,
this.subnetId,
this.securityGroupIds,
this.userData,
this.tags,
}) : super(/* ... */);
}
class Container extends Resource {
final String name;
final String image;
final int? replicas;
final Map<String, String>? environment;
final List<Port>? ports;
Container({
required this.name,
required this.image,
this.replicas,
this.environment,
this.ports,
}) : super(/* ... */);
}class Database extends Resource {
final String name;
final String engine;
final String version;
final String instanceClass;
final int storage;
final int? backupRetention;
final bool multiAz;
final String? subnetGroup;
final List<String>? securityGroups;
Database({
required this.name,
required this.engine,
required this.version,
required this.instanceClass,
required this.storage,
this.backupRetention,
this.multiAz = false,
this.subnetGroup,
this.securityGroups,
}) : super(/* ... */);
}
class ObjectStorage extends Resource {
final String name;
final String? region;
final bool? versioning;
final String? encryption;
ObjectStorage({
required this.name,
this.region,
this.versioning,
this.encryption,
}) : super(/* ... */);
}class Network extends Resource {
final String name;
final String cidr;
final Map<String, String>? tags;
Network({
required this.name,
required this.cidr,
this.tags,
}) : super(/* ... */);
}
class Subnet extends Resource {
final String name;
final String networkId;
final String cidr;
final String? availabilityZone;
Subnet({
required this.name,
required this.networkId,
required this.cidr,
this.availabilityZone,
}) : super(/* ... */);
}
class SecurityGroup extends Resource {
final String name;
final String networkId;
final List<IngressRule>? ingressRules;
final List<EgressRule>? egressRules;
SecurityGroup({
required this.name,
required this.networkId,
this.ingressRules,
this.egressRules,
}) : super(/* ... */);
}Resources should validate their properties:
class VirtualMachine extends Resource {
@override
void validate() {
if (name.isEmpty) {
throw ValidationException('VM name cannot be empty');
}
if (!RegExp(r'^[a-z0-9-]+$').hasMatch(name)) {
throw ValidationException(
'VM name must contain only lowercase letters, numbers, and hyphens',
);
}
if (!['small', 'medium', 'large', 'xlarge'].contains(size)) {
throw ValidationException('Invalid VM size: $size');
}
if (subnetId != null && !subnetId!.startsWith('subnet.')) {
throw ValidationException('Invalid subnet ID format');
}
}
}Resources must be serializable for state management:
class VirtualMachine extends Resource {
@override
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type,
'properties': {
'name': name,
'image': image,
'size': size,
'subnetId': subnetId,
'securityGroupIds': securityGroupIds,
'tags': tags,
},
'dependencies': dependencies,
};
}
factory VirtualMachine.fromJson(Map<String, dynamic> json) {
final props = json['properties'] as Map<String, dynamic>;
return VirtualMachine(
name: props['name'] as String,
image: props['image'] as String,
size: props['size'] as String,
subnetId: props['subnetId'] as String?,
securityGroupIds: (props['securityGroupIds'] as List?)?.cast<String>(),
tags: (props['tags'] as Map?)?.cast<String, String>(),
);
}
}Use resource references to link resources:
class ResourceReference {
final String resourceId;
final String? attribute;
ResourceReference(this.resourceId, [this.attribute]);
String get value {
// Resolve reference at runtime
return _resolveReference();
}
}
// Usage
final network = Network(name: 'main-network');
final subnet = Subnet(
name: 'public-subnet',
networkId: network.id, // Direct reference
);void main() {
final stack = DrftStack(
name: 'web-application',
providers: [
AwsProvider(region: 'us-east-1'),
],
resources: [
// Network
Vpc(
name: 'main-vpc',
cidr: '10.0.0.0/16',
tags: {'Environment': 'production'},
),
// Subnets
Subnet(
name: 'public-1',
vpcId: vpc.id,
cidr: '10.0.1.0/24',
availabilityZone: 'us-east-1a',
),
Subnet(
name: 'public-2',
vpcId: vpc.id,
cidr: '10.0.2.0/24',
availabilityZone: 'us-east-1b',
),
// Security Groups
SecurityGroup(
name: 'web-sg',
vpcId: vpc.id,
ingressRules: [
IngressRule(port: 80, protocol: 'tcp', source: '0.0.0.0/0'),
IngressRule(port: 443, protocol: 'tcp', source: '0.0.0.0/0'),
],
),
SecurityGroup(
name: 'db-sg',
vpcId: vpc.id,
ingressRules: [
IngressRule(port: 5432, protocol: 'tcp', source: '10.0.0.0/16'),
],
),
// Load Balancer
LoadBalancer(
name: 'web-lb',
subnets: [subnet1.id, subnet2.id],
securityGroups: [webSg.id],
listeners: [
Listener(port: 80, protocol: 'http'),
Listener(port: 443, protocol: 'https'),
],
),
// Virtual Machines
VirtualMachine(
name: 'web-1',
image: 'ubuntu-22.04',
size: 'medium',
subnetId: subnet1.id,
securityGroupIds: [webSg.id],
tags: {'Role': 'web-server'},
),
VirtualMachine(
name: 'web-2',
image: 'ubuntu-22.04',
size: 'medium',
subnetId: subnet2.id,
securityGroupIds: [webSg.id],
tags: {'Role': 'web-server'},
),
// Database
Database(
name: 'app-db',
engine: 'postgresql',
version: '14',
instanceClass: 'db.t3.medium',
storage: 100,
backupRetention: 7,
multiAz: true,
subnetGroup: dbSubnetGroup.id,
securityGroups: [dbSg.id],
tags: {'Environment': 'production'},
),
],
);
}Some resources represent external infrastructure that cannot be created, updated, or deleted through DRFT. These are called read-only resources and can be marked using either the ReadOnlyResource base class or the ReadOnly mixin.
Data sources are appropriate for:
- Resources managed outside DRFT (e.g., Firebase projects created via Console)
- External APIs that provide reference data
- Existing infrastructure that should not be modified
- Resources that other resources depend on for information
Extend ReadOnlyResource instead of Resource:
class FirebaseProjectState extends ResourceState {
FirebaseProjectState({required super.resource});
}
class FirebaseProject extends ReadOnlyResource<FirebaseProjectState> {
final String projectId;
final String displayName;
const FirebaseProject({
required String id,
required this.projectId,
required this.displayName,
List<Resource> dependencies = const [],
}) : super(
id: id,
dependencies: dependencies,
);
}If your resource extends a base class (e.g., for typed provider support), use the ReadOnly mixin:
/// Base class for Firebase resources (for typed provider)
abstract class FirebaseResource extends Resource<ResourceState> {
const FirebaseResource({
required super.id,
super.dependencies = const [],
});
}
/// Firebase Project with ReadOnly mixin
class FirebaseProject extends FirebaseResource with ReadOnly {
final String projectId;
final String displayName;
const FirebaseProject({
required String id,
required this.projectId,
required this.displayName,
List<Resource> dependencies = const [],
}) : super(id: id);
}Both approaches are automatically detected by the planner and executor, so no additional configuration is needed.
- Planning: Data sources are never included in create, update, or delete operations
- Reading: Data sources are read to verify they exist and get current state
- Dependencies: Other resources can depend on data sources normally
- Refresh: Data sources are refreshed during state refresh operations
final stack = DrftStack(
name: 'my-stack',
providers: [FirebaseProvider()],
resources: [
// Data source - cannot be created/updated/deleted
FirebaseProject(
id: 'project',
projectId: 'my-project-id', // Must exist externally
displayName: 'My Project',
),
// Regular resource - depends on data source
FirebaseApp(
id: 'ios-app',
projectId: 'my-project-id', // References the project
platform: FirebaseAppPlatform.ios,
displayName: 'iOS App',
bundleId: 'com.example.app',
),
],
);
// Plan will only include operations for FirebaseApp
// FirebaseProject will be read to verify it exists
final plan = await stack.plan();Providers must implement readResource for read-only resources to verify they exist externally:
@override
Future<ResourceState> readResource(Resource resource) async {
if (resource is FirebaseProject) {
// Verify the project exists in Firebase
final project = await _firebaseManagement.projects.getProject(
resource.projectId,
);
if (project == null) {
throw ResourceNotFoundException(resource.projectId);
}
return FirebaseProjectState(
resource: resource,
);
}
// ... handle other resources
}Note: Providers do not need to handle createResource, updateResource, or deleteResource for read-only resources, as the planner and executor guarantee these methods will never be called for resources with the ReadOnly mixin or ReadOnlyResource instances.
- Immutability: Resources should be immutable with final properties
- Type Safety: Use strong typing for all properties
- Validation: Validate all inputs in the
validate()method - Defaults: Provide sensible defaults where appropriate
- Documentation: Document all properties and their purposes
- Serialization: Ensure proper JSON serialization/deserialization
- Dependencies: Make dependencies explicit and clear
- Flutter-style: Follow Flutter widget patterns for familiarity
- Data Sources: Use
ReadOnlyResourceor theReadOnlymixin for read-only external resources