diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml new file mode 100644 index 0000000..3a27adb --- /dev/null +++ b/.github/workflows/analyze.yml @@ -0,0 +1,21 @@ +name: Analyze + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + - name: Install dependencies + run: dart pub get + - name: Analyze + run: dart analyze \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2075bf6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,14 @@ +name: Publish to pub.dev + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+*' + +jobs: + publish: + permissions: + id-token: write + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 + with: + environment: pub.dev diff --git a/example/utopia_orchestration_example.dart b/example/utopia_orchestration_example.dart index ebb3b5b..4a4d239 100644 --- a/example/utopia_orchestration_example.dart +++ b/example/utopia_orchestration_example.dart @@ -1,12 +1,13 @@ import 'package:utopia_orchestration/utopia_orchestration.dart'; void main() async { - final cli = DockerCLI(); - // cli.remove('test-hello-world'); - final res = await cli.run('hello-world', 'test-hello-world'); + final dockerCli = DockerCLI(); + final orchestrator = Orchestration(dockerCli); + final res = + await orchestrator.run(image: 'hello-world', name: 'test-hello-world'); print(res); - print(await cli.list()); - final stats = await cli.getStats(container: 'test-hello-world'); + print(await orchestrator.list()); + final stats = await orchestrator.getStats(); print(stats); - await cli.remove('test-hello-world'); + await orchestrator.remove('test-hello-world'); } diff --git a/lib/src/adapter.dart b/lib/src/adapter.dart new file mode 100644 index 0000000..2ffe476 --- /dev/null +++ b/lib/src/adapter.dart @@ -0,0 +1,152 @@ +import 'stats.dart'; +import 'container.dart'; + +/// An abstract class to define a generic adapter interface for orchestrating containers. +/// This class provides the structure for managing container lifecycle, networking, and resource allocation. +abstract class Adapter { + /// The namespace to isolate resources within. + String namespace = 'utopia'; + + /// The number of CPU cores allocated to the container. Defaults to 0, meaning not specified. + int cpus = 0; + + /// The amount of memory allocated to the container in MB. Defaults to 0, meaning not specified. + int memory = 0; + + /// The amount of swap space allocated to the container in MB. Defaults to 0, meaning not specified. + int swap = 0; + + /// Filters environment variable keys to ensure they only contain valid characters. + /// + /// [string] The input string to filter. + /// Returns a string with only valid characters for environment variable keys. + String filterEnvKey(String string) { + return string.replaceAll(RegExp('[^A-Za-z0-9_\\.-]'), ''); + } + + /// Creates a network. + /// + /// [name] The name of the network. + /// [internal] Specifies whether the network is internal. Defaults to false. + /// Returns a Future indicating success or failure. + Future createNetwork(String name, {bool internal = false}); + + /// Removes a network. + /// + /// [name] The name of the network to remove. + /// Returns a Future indicating success or failure. + Future removeNetwork(String name); + + /// Connects a container to a network. + /// + /// [container] The container to connect. + /// [network] The network to connect to. + /// Returns a Future indicating success or failure. + Future networkConnect(String container, String network); + + /// Disconnects a container from a network. + /// + /// [container] The container to disconnect. + /// [network] The network to disconnect from. + /// [force] Whether to forcibly disconnect the container. Defaults to false. + /// Returns a Future indicating success or failure. + Future networkDisconnect(String container, String network, + {bool force = false}); + + /// Lists available networks. + /// + /// Returns a Future> of network information. + Future> listNetworks(); + + /// Retrieves usage statistics for containers. + /// + /// [container] Optional specific container ID to get stats for. If null, stats for all containers are returned. + /// [filters] Optional filters to apply. + /// Returns a Future> of container statistics. + Future> getStats( + {String? container, Map? filters}); + + /// Pulls an image. + /// + /// [image] The image to pull. + /// Returns a Future indicating success or failure. + Future pull(String image); + + /// Lists containers. + /// + /// [filters] Optional filters to apply. + /// Returns a Future> of container information. + Future> list({Map? filters}); + + /// Runs a container. + /// + /// This method creates and runs a new container, returning a Future containing the container ID on success. + Future run( + String image, + String name, { + List? command, + String entrypoint = '', + String workdir = '', + List? volumes, + Map? vars, + String mountFolder = '', + Map? labels, + String hostname = '', + bool remove = false, + String network = '', + }); + + /// Executes a command in a container. + /// + /// [name] The container name. + /// [command] The command to execute. + /// [vars] Optional environment variables to set. + /// [timeout] Optional timeout in seconds. + /// Returns a Future indicating success or failure. + Future execute(String name, List command, + {Map? vars, int timeout = -1}); + + /// Removes a container. + /// + /// [name] The name of the container to remove. + /// [force] Whether to forcibly remove the container. Defaults to false. + /// Returns a Future indicating success or failure. + Future remove(String name, {bool force = false}); + + /// Sets the namespace for container operations. + /// + /// [namespace] The namespace to set. + /// Returns the current instance for chaining. + Adapter setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + /// Sets the number of CPU cores for container operations. + /// + /// [cpus] The number of CPU cores to allocate. + /// Returns the current instance for chaining. + Adapter setCpus(int cpus) { + this.cpus = cpus; + + return this; + } + + /// Sets the amount of memory for container operations. + /// + /// [memory] The amount of memory in MB to allocate. + /// Returns the current instance for chaining. + Adapter setMemory(int memory) { + this.memory = memory; + return this; + } + + /// Sets the amount of swap memory for container operations. + /// + /// [swap] The amount of swap memory in MB to allocate. + /// Returns the current instance for chaining. + Adapter setSwap(int swap) { + this.swap = swap; + return this; + } +} diff --git a/lib/src/adapters/docker_cli.dart b/lib/src/adapters/docker_cli.dart new file mode 100644 index 0000000..0db6de1 --- /dev/null +++ b/lib/src/adapters/docker_cli.dart @@ -0,0 +1,352 @@ +import 'dart:convert'; +import 'dart:io'; + +import '../adapter.dart'; +import '../stats.dart'; +import '../container.dart'; + +class DockerCLI extends Adapter { + Future _execute(String command, List arguments, + {String? workingDirectory}) async { + return await Process.run(command, arguments, + workingDirectory: workingDirectory, runInShell: true); + } + + DockerCLI({String? username, String? password}) { + if (username != null && password != null) { + _execute('docker', ['login', '--username', username, '--password-stdin'], + workingDirectory: '.') + .then((result) { + if (result.exitCode != 0) { + throw Exception("Docker Error: ${result.stderr}"); + } + }); + } + } + + @override + Future createNetwork(String name, {bool internal = false}) async { + var command = ['network', 'create', name]; + if (internal) { + command.add('--internal'); + } + var result = await _execute('docker', command); + return result.exitCode == 0; + } + + @override + Future removeNetwork(String name) async { + var result = await _execute('docker', ['network', 'rm', name]); + return result.exitCode == 0; + } + + @override + Future networkConnect(String container, String network) async { + var result = + await _execute('docker', ['network', 'connect', network, container]); + return result.exitCode == 0; + } + + @override + Future networkDisconnect(String container, String network, + {bool force = false}) async { + var command = ['network', 'disconnect']; + if (force) { + command.add('--force'); + } + command.addAll([network, container]); + var result = await _execute('docker', command); + return result.exitCode == 0; + } + + @override + Future> getStats( + {String? container, Map? filters}) async { + List containerIds = []; + + if (container == null) { + // Assuming `list` returns a Future> where Container is a Dart class similar to the PHP version + var containers = await list(filters: filters); + containerIds = containers.map((c) => c.id).toList(); + } else { + containerIds.add(container); + } + + if (containerIds.isEmpty && filters != null && filters.isNotEmpty) { + return []; // No containers found + } + + var result = await Process.run( + 'docker', + [ + 'stats', + '--no-trunc', + '--format', + 'json', + '--no-stream', + ...containerIds, + ], + runInShell: true, + ); + + if (result.exitCode != 0) { + throw Exception("Docker Error: ${result.stderr}"); + } + + List stats = []; + var lines = result.stdout.split('\n'); + + for (var line in lines) { + if (line.isEmpty) { + continue; + } + + var data = jsonDecode(line); + + stats.add(Stats.fromJson({ + 'containerId': data['ID'], + 'containerName': data['Name'], + 'cpuUsage': + double.tryParse(data['CPUPerc']!.replaceAll('%', '')) ?? 0.0 / 100, + 'memoryUsage': + double.tryParse(data['MemPerc']!.replaceAll('%', '')) ?? 0.0, + 'diskIO': parseIOStats(data['BlockIO']!), + 'memoryIO': parseIOStats(data['MemUsage']!), + 'networkIO': parseIOStats(data['NetIO']!), + })); + } + + return stats; + } + + @override + Future> listNetworks() async { + var result = await _execute('docker', [ + 'network', + 'ls', + '--format', + '{{.ID}} {{.Name}} {{.Driver}} {{.Scope}}' + ]); + if (result.exitCode != 0) { + throw Exception("Docker Error: ${result.stderr}"); + } + return LineSplitter.split(result.stdout).map((line) { + var parts = line.split(' '); + return { + 'id': parts[0], + 'name': parts[1], + 'driver': parts[2], + 'scope': parts[3], + }; + }).toList(); + } + + @override + Future pull(String image) async { + var result = await _execute('docker', ['pull', image]); + return result.exitCode == 0; + } + + @override + Future> list({Map? filters}) async { + List command = ['ps', '--all', '--format', 'json']; + if (filters != null) { + filters.forEach((key, value) { + command.add('--filter'); + command.add('$key=$value'); + }); + } + var result = await _execute('docker', command); + if (result.exitCode != 0) { + throw Exception("Docker Error: ${result.stderr}"); + } + return LineSplitter.split(result.stdout).map((line) { + var details = jsonDecode(line); + return Container.fromJson({ + 'id': details['ID'], + 'name': details['Names'], + 'status': details['Status'], + 'labels': _parseLabels(details['Labels'] ?? ''), + }); + }).toList(); + } + + Map _parseLabels(String input) { + var pairs = input.split(','); + var map = {}; + + for (var pair in pairs) { + var keyValue = pair.split('='); + if (keyValue.length == 2) { + map[keyValue[0]] = keyValue[1]; + } + } + + return map; + } + + @override + Future run( + String image, + String name, { + List? command, + String entrypoint = '', + String workdir = '', + List? volumes, + Map? vars, + String mountFolder = '', + Map? labels, + String hostname = '', + bool remove = false, + String network = '', + }) async { + var commandList = command + ?.map((value) => value.contains(' ') ? "'$value'" : value) + .toList() ?? + []; + var labelString = labels?.entries.map((entry) { + var label = entry.value.replaceAll("'", ""); + return '--label ${entry.key}=${label.contains(' ') ? "'$label'" : label}'; + }).join(' ') ?? + ''; + + var varsList = vars?.entries.map((entry) { + var key = filterEnvKey(entry.key); + var value = entry.value.isEmpty ? '' : entry.value; + return '--env $key=$value'; + }).toList() ?? + []; + + var volumeString = + volumes?.map((volume) => '--volume $volume ').join(' ') ?? ''; + + var time = DateTime.now().millisecondsSinceEpoch; + + var runArguments = [ + 'run', + '-d', + if (remove) '--rm', + if (network.isNotEmpty) '--network="$network"', + if (entrypoint.isNotEmpty) '--entrypoint="$entrypoint"', + if (cpus > 0) '--cpus=$cpus', + if (memory > 0) '--memory=${memory}m', + if (swap > 0) '--memory-swap=${swap}m', + '--label=$namespace-created=$time', + '--name=$name', + if (mountFolder.isNotEmpty) '--volume $mountFolder:/tmp:rw', + if (volumeString.isNotEmpty) volumeString, + if (labelString.isNotEmpty) labelString, + if (workdir.isNotEmpty) '--workdir $workdir', + if (hostname.isNotEmpty) '--hostname $hostname', + ...varsList, + image, + ...commandList, + ].where((element) => element.isNotEmpty).toList(); + + var result = await _execute('docker', runArguments); + if (result.exitCode != 0) { + throw Exception("Docker Error: ${result.stderr}"); + } + + return result.stdout.trim(); + } + + /// Executes a command in a specified container. + /// + /// [name] The name of the container where the command will be executed. + /// [command] The command to execute as a list of strings. + /// [vars] Optional map of environment variables to set in the format of { 'KEY': 'VALUE' }. + /// [timeout] Optional timeout in seconds for how long to wait for the command to execute. A value of -1 indicates no timeout. + /// Returns a Future indicating success or failure of the command execution. + /// + /// Throws an Exception if the command fails or times out. + @override + Future execute( + String name, + List command, { + Map? vars, + int timeout = -1, + }) async { + var commandList = command + .map((value) => value.contains(' ') ? "'$value'" : value) + .toList(); + var varsList = vars?.entries.map((entry) { + var key = filterEnvKey(entry.key); + var value = entry.value.isEmpty ? '' : entry.value; + return '--env $key=$value'; + }).toList() ?? + []; + + var processResult = await Process.run( + 'docker', + ['exec', ...varsList, name, ...commandList], + runInShell: true, + environment: vars, + ); + + if (processResult.exitCode != 0) { + if (processResult.exitCode == 124) { + throw Exception('Command timed out'); + } else { + throw Exception("Docker Error: ${processResult.stderr}"); + } + } + + return processResult.exitCode == 0; + } + + @override + Future remove(String name, {bool force = false}) async { + List command = ['rm']; + if (force) { + command.add('--force'); + } + command.add(name); + var result = await _execute('docker', command); + if (result.exitCode != 0 || !result.stdout.contains(name)) { + throw Exception("Docker Error: ${result.stderr}"); + } + return true; + } + + Map parseIOStats(String stats) { + var units = { + 'B': 1, + 'KB': 1000, + 'MB': 1000000, + 'GB': 1000000000, + 'TB': 1000000000000, + 'KiB': 1024, + 'MiB': 1048576, + 'GiB': 1073741824, + 'TiB': 1099511627776, + }; + + var parts = stats.split(' / '); + var inStr = parts[0]; + var outStr = parts[1]; + + String? inUnit; + String? outUnit; + + units.forEach((unit, value) { + if (inStr.endsWith(unit)) { + inUnit = unit; + } + if (outStr.endsWith(unit)) { + outUnit = unit; + } + }); + + var inMultiply = inUnit == null ? 1 : units[inUnit]!; + var outMultiply = outUnit == null ? 1 : units[outUnit]!; + + var inValue = double.parse(inStr.replaceAll(inUnit ?? '', '').trim()); + var outValue = double.parse(outStr.replaceAll(outUnit ?? '', '').trim()); + + return { + 'in': inValue * inMultiply, + 'out': outValue * outMultiply, + }; + } +} diff --git a/lib/src/container.dart b/lib/src/container.dart new file mode 100644 index 0000000..0e01567 --- /dev/null +++ b/lib/src/container.dart @@ -0,0 +1,77 @@ +import 'package:collection/collection.dart'; + +/// A Dart class for representing a container within a container orchestration environment. +/// This class encapsulates information about a container, such as its name, ID, status, and labels. +class Container { + /// The name of the container. + String name; + + /// The unique identifier of the container. + String id; + + /// The current status of the container. + String status; + + /// A map of labels associated with the container. + Map labels; + + /// Constructs a [Container] instance with optional parameters. + Container({ + this.name = '', + this.id = '', + this.status = '', + Map? labels, + }) : labels = labels ?? {}; + + /// Creates a new [Container] instance from a JSON map. + Container.fromJson(Map json) + : name = json['name'] ?? '', + id = json['id'] ?? '', + status = json['status'] ?? '', + labels = json['labels'] ?? {}; + + /// Returns a JSON map representation of the [Container] instance. + Map toJson() { + return { + 'name': name, + 'id': id, + 'status': status, + 'labels': labels, + }; + } + + /// Creates a copy of the current [Container] instance with the given overridden parameters. + Container copyWith({ + String? name, + String? id, + String? status, + Map? labels, + }) { + return Container( + name: name ?? this.name, + id: id ?? this.id, + status: status ?? this.status, + labels: labels ?? Map.from(this.labels), + ); + } + + @override + String toString() { + return 'Container(name: $name, id: $id, status: $status, labels: $labels)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Container && + other.name == name && + other.id == id && + other.status == status && + MapEquality().equals(other.labels, labels); + } + + @override + int get hashCode => + name.hashCode ^ id.hashCode ^ status.hashCode ^ labels.hashCode; +} diff --git a/lib/src/network.dart b/lib/src/network.dart new file mode 100644 index 0000000..385b0cb --- /dev/null +++ b/lib/src/network.dart @@ -0,0 +1,75 @@ +/// A Dart class for representing a network within a container orchestration environment. +/// This class encapsulates information about a network, such as its name, ID, driver, and scope. +class Network { + /// The name of the network. + String name; + + /// The unique identifier of the network. + String id; + + /// The driver used by the network. + String driver; + + /// The scope of the network. + String scope; + + /// Constructs a [Network] instance with optional parameters. + Network({ + this.name = '', + this.id = '', + this.driver = '', + this.scope = '', + }); + + /// Creates a new [Network] instance from a JSON map. + Network.fromJson(Map json) + : name = json['name'] ?? '', + id = json['id'] ?? '', + driver = json['driver'] ?? '', + scope = json['scope'] ?? ''; + + /// Returns a JSON map representation of the [Network] instance. + Map toJson() { + return { + 'name': name, + 'id': id, + 'driver': driver, + 'scope': scope, + }; + } + + /// Creates a copy of the current [Network] instance with the given overridden parameters. + Network copyWith({ + String? name, + String? id, + String? driver, + String? scope, + }) { + return Network( + name: name ?? this.name, + id: id ?? this.id, + driver: driver ?? this.driver, + scope: scope ?? this.scope, + ); + } + + @override + String toString() { + return 'Network(name: $name, id: $id, driver: $driver, scope: $scope)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Network && + other.name == name && + other.id == id && + other.driver == driver && + other.scope == scope; + } + + @override + int get hashCode => + name.hashCode ^ id.hashCode ^ driver.hashCode ^ scope.hashCode; +} diff --git a/lib/src/orchestration.dart b/lib/src/orchestration.dart new file mode 100644 index 0000000..c503d05 --- /dev/null +++ b/lib/src/orchestration.dart @@ -0,0 +1,134 @@ +import 'adapter.dart'; // Assume this is the Dart adaptation of the Adapter class +import 'stats.dart'; // Assume this contains the Dart version of the Stats class +import 'container.dart'; // Assume this contains the Dart version of the Container class + +/// A Dart class for orchestrating containers within a container orchestration environment. +/// This class provides methods for network management, container management, and retrieving container statistics. +class Orchestration { + /// The adapter for the specific orchestration backend (e.g., Docker). + final Adapter adapter; + + /// Constructs an [Orchestration] instance with the required [adapter]. + Orchestration(this.adapter); + + /// Parses a command string into an array of arguments to handle spaces and quotes properly. + List parseCommandString(String command) { + // This functionality is largely provided by the shell itself in Dart, + // so a direct implementation might not be necessary. Instead, consider using Process.run() + // where arguments are passed as a list and Dart handles the parsing. + return command.split(' '); // Simplified for illustration + } + + /// Creates a network. + Future createNetwork(String name, {bool internal = false}) async { + return adapter.createNetwork(name, internal: internal); + } + + /// Removes a network. + Future removeNetwork(String name) async { + return adapter.removeNetwork(name); + } + + /// Lists available networks. + Future> listNetworks() async { + return adapter.listNetworks(); + } + + /// Connects a container to a network. + Future networkConnect(String container, String network) async { + return adapter.networkConnect(container, network); + } + + /// Gets usage statistics for containers. + Future> getStats( + {String? container, Map? filters}) async { + return adapter.getStats(container: container, filters: filters); + } + + /// Disconnects a container from a network. + Future networkDisconnect(String container, String network, + {bool force = false}) async { + return adapter.networkDisconnect(container, network, force: force); + } + + /// Pulls an image. + Future pull(String image) async { + return adapter.pull(image); + } + + /// Lists containers. + Future> list({Map? filters}) async { + return adapter.list(filters: filters); + } + + /// Runs a container. + Future run({ + required String image, + required String name, + List? command, + String entrypoint = '', + String workdir = '', + List? volumes, + Map? vars, + String mountFolder = '', + Map? labels, + String hostname = '', + bool remove = false, + String network = '', + }) async { + return adapter.run( + image, + name, + command: command, + entrypoint: entrypoint, + workdir: workdir, + volumes: volumes, + vars: vars, + mountFolder: mountFolder, + labels: labels, + hostname: hostname, + remove: remove, + network: network, + ); + } + + /// Executes a command in a container. + Future execute({ + required String name, + required List command, + required String output, + Map? vars, + int timeout = -1, + }) async { + return adapter.execute(name, command, vars: vars, timeout: timeout); + } + + /// Removes a container. + Future remove(String name, {bool force = false}) async { + return adapter.remove(name, force: force); + } + + /// Sets the namespace for containers. + Orchestration setNamespace(String namespace) { + adapter.setNamespace(namespace); + return this; + } + + /// Sets the maximum allowed CPU cores per container. + Orchestration setCpus(int cores) { + adapter.setCpus(cores); + return this; + } + + /// Sets the maximum allowed memory in MB per container. + Orchestration setMemory(int mb) { + adapter.setMemory(mb); + return this; + } + + /// Sets the maximum allowed swap memory in MB per container. + Orchestration setSwap(int mb) { + adapter.setSwap(mb); + return this; + } +} diff --git a/lib/src/stats.dart b/lib/src/stats.dart new file mode 100644 index 0000000..24f8944 --- /dev/null +++ b/lib/src/stats.dart @@ -0,0 +1,89 @@ +import 'package:collection/collection.dart'; + +/// A Dart class for representing the statistics of a container within a container orchestration environment. +/// This class encapsulates detailed performance metrics such as CPU usage, memory usage, and IO statistics. +class Stats { + /// The unique identifier of the container. + final String containerId; + + /// The name of the container. + final String containerName; + + /// The CPU usage percentage of the container. + final double cpuUsage; + + /// The memory usage of the container in megabytes. + final double memoryUsage; + + /// Disk IO statistics of the container, including read and write speeds. + final Map diskIO; + + /// Memory IO statistics of the container, including usage and available memory. + final Map memoryIO; + + /// Network IO statistics of the container, including incoming and outgoing traffic. + final Map networkIO; + + /// Constructs a [Stats] instance with required parameters. + Stats({ + required this.containerId, + required this.containerName, + required this.cpuUsage, + required this.memoryUsage, + required this.diskIO, + required this.memoryIO, + required this.networkIO, + }); + + /// Creates a new [Stats] instance from a JSON map. + Stats.fromJson(Map json) + : containerId = json['containerId'], + containerName = json['containerName'], + cpuUsage = json['cpuUsage'].toDouble(), + memoryUsage = json['memoryUsage'].toDouble(), + diskIO = Map.from(json['diskIO']), + memoryIO = Map.from(json['memoryIO']), + networkIO = Map.from(json['networkIO']); + + /// Returns a JSON map representation of the [Stats] instance. + Map toJson() { + return { + 'containerId': containerId, + 'containerName': containerName, + 'cpuUsage': cpuUsage, + 'memoryUsage': memoryUsage, + 'diskIO': diskIO, + 'memoryIO': memoryIO, + 'networkIO': networkIO, + }; + } + + @override + String toString() { + return 'Stats(containerId: $containerId, containerName: $containerName, cpuUsage: $cpuUsage, memoryUsage: $memoryUsage, diskIO: $diskIO, memoryIO: $memoryIO, networkIO: $networkIO)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Stats && + other.containerId == containerId && + other.containerName == containerName && + other.cpuUsage == cpuUsage && + other.memoryUsage == memoryUsage && + MapEquality().equals(other.diskIO, diskIO) && + MapEquality().equals(other.memoryIO, memoryIO) && + MapEquality().equals(other.networkIO, networkIO); + } + + @override + int get hashCode => + containerId.hashCode ^ + containerName.hashCode ^ + cpuUsage.hashCode ^ + memoryUsage.hashCode ^ + diskIO.hashCode ^ + memoryIO.hashCode ^ + networkIO.hashCode; +} diff --git a/lib/src/utopia_orchestration_base.dart b/lib/src/utopia_orchestration_base.dart deleted file mode 100644 index e8a6f15..0000000 --- a/lib/src/utopia_orchestration_base.dart +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: Put public facing types in this file. - -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; -} diff --git a/lib/utopia_orchestration.dart b/lib/utopia_orchestration.dart index cacc50b..f74d668 100644 --- a/lib/utopia_orchestration.dart +++ b/lib/utopia_orchestration.dart @@ -3,5 +3,9 @@ /// More dartdocs go here. library; -export 'src/utopia_orchestration_base.dart'; -export 'src/docker_cli.dart'; +export 'src/orchestration.dart'; +export 'src/adapters/docker_cli.dart'; +export 'src/adapter.dart'; +export 'src/container.dart'; +export 'src/network.dart'; +export 'src/stats.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index b2462f9..4be9d1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: # Add regular dependencies here. dependencies: + collection: ^1.18.0 # path: ^1.8.0 dev_dependencies: diff --git a/test/utopia_orchestration_test.dart b/test/utopia_orchestration_test.dart deleted file mode 100644 index e3f4799..0000000 --- a/test/utopia_orchestration_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:utopia_orchestration/utopia_orchestration.dart'; -import 'package:test/test.dart'; - -void main() { - group('A group of tests', () { - final awesome = Awesome(); - - setUp(() { - // Additional setup goes here. - }); - - test('First Test', () { - expect(awesome.isAwesome, isTrue); - }); - }); -}