-
Notifications
You must be signed in to change notification settings - Fork 50
Description
Hello, I just started using this wonderful package to help me handle and simplify a piece of code with lots of error handling.
If possible could someone give me a review regarding my usage of fpdart in the following code extracts ?
The following pieces of code are used to automate the deployment of CI artifacts on prototypes. This is done by connecting to the device through SSH. A few details that comes up:
- Every single command called necessitate error handling.
- Do notation does make my composed functions (in the code bellow
doSpoofServiceisspoofServicewritten in Do.depositArtifactwould be my next candidate for that treatment.) way more compact and readable but I do not know if I'm using it right. - My code is not strictly functional since my function access the encompassing class properties. If I'm not mistaken I would have to use some flavor of
Readerto handle this properly ?
Any comment is welcome, and I wouldn't mind documenting whatever discussion comes out of this and open a PR to help improve the documentation.
The interface used by the following classes , nothing very interesting here for completion purpose:
abstract interface class DeployStrategy {
TaskEither<DeployStrategyError, Unit> execute();
Stream<String> get logStream;
}
sealed class DeployStrategyError {
final Exception? e;
final StackTrace? s;
DeployStrategyError({
this.e,
this.s,
});
String get errorMessage;
}
class ExecuteImplementationError extends DeployStrategyError {
ExecuteImplementationError({super.e, super.s});
@override
String get errorMessage => 'An error occurred while using the execute() method of an implementation. With message: $e';
}
An abstract child of the base class, used to provide common functionnalities to the final implementations:
abstract class BaseSSHStrategy implements DeployStrategy {
BaseSSHStrategy({
required logger,
}) : _logger = logger;
final ExfoLogger _logger;
ExfoLogger get logger => _logger;
final StreamController<String> _logStreamController = StreamController();
@override
Stream<String> get logStream => _logStreamController.stream;
// Connection
late String ip;
late int port;
late String serviceUser;
late String rootUser;
// Artifact
late String artifactFileToUpload;
late String archive;
late String temporaryFolder;
late String folderToDeploy;
late String? deployableFolderParent;
late String deploymentLocation;
// Service
late String serviceName;
late String serviceNameWithoutUser;
late String workingDirectory;
late String serviceExecStart;
void init({
required String ip,
int port = 22,
required String serviceUser,
required String rootUser,
required String artifactFileToUpload,
required String archive,
String temporaryFolder = 'temp',
required String folderToDeploy,
String? deployableFolderParent,
required String deploymentLocation,
required String serviceName,
required String serviceNameWithoutUser,
required String workingDirectory,
required String serviceExecStart,
}) {
this.ip = ip;
this.port = port;
this.serviceUser = serviceUser;
this.rootUser = rootUser;
this.artifactFileToUpload = artifactFileToUpload;
this.archive = archive;
this.temporaryFolder = temporaryFolder;
this.folderToDeploy = folderToDeploy;
this.deployableFolderParent = deployableFolderParent;
this.deploymentLocation = deploymentLocation;
this.serviceName = serviceName;
this.serviceNameWithoutUser = serviceNameWithoutUser;
this.workingDirectory = workingDirectory;
this.serviceExecStart = serviceExecStart;
}
void _log(String toLog) {
_logStreamController.add(toLog);
_logger.debug(toLog);
}
/// Orchestration
TaskEither<SSHCommandError, Unit> doSpoofService({
bool verbose = true,
}) {
return TaskEither<SSHCommandError, Unit>.Do(($) async {
final client = await $(createSSHClient(ip, port, rootUser));
final serviceFile = '/etc/systemd/system/$serviceName.service';
final serviceFileExist = await $(doesFileExists(client, serviceFile));
if (!serviceFileExist) await $(createCustomService(client, serviceName, verbose: true));
await $(changeServiceWorkingDirectory(client, serviceFile, workingDirectory));
await $(changeServiceExecStart(client, serviceFile, serviceExecStart));
if (verbose) await $(printSftpFile(client, serviceFile));
await $(reloadDaemon(client));
await $(restartService(client, serviceName));
await $(closeSSHClient(client).toTaskEither());
return await $(TaskEither.of(unit));
});
}
TaskEither<SSHCommandError, Unit> spoofService({
bool verbose = true,
}) {
final request = createSSHClient(ip, port, rootUser).flatMap(
(client) {
final serviceFile = '/etc/systemd/system/$serviceName.service';
return doesFileExists(client, serviceFile).flatMap(
(exist) {
if (!exist) return createCustomService(client, serviceName, verbose: true);
return TaskEither.of(unit);
},
).flatMap(
(_) => changeServiceWorkingDirectory(
client,
serviceFile,
workingDirectory,
).flatMap(
(_) => changeServiceExecStart(
client,
serviceFile,
serviceExecStart,
)
.flatMap(
(_) {
if (verbose) return printSftpFile(client, serviceFile);
return TaskEither.of(unit);
},
)
.flatMap(
(_) => reloadDaemon(client).flatMap(
(_) => restartService(client, serviceName),
),
)
.flatMap(
(_) => closeSSHClient(client).toTaskEither(),
),
),
);
},
);
return request;
}
TaskEither<SSHCommandError, Unit> depositArtifact({
bool verbose = false,
}) {
final request = createSSHClient(ip, port, rootUser).flatMap(
(client) => stopService(client, serviceName).flatMap(
(_) => closeSSHClient(client).toTaskEither().flatMap(
(_) => createSSHClient(ip, port, serviceUser).flatMap(
(client) => deleteFolderIfExist(
client,
temporaryFolder,
verbose: verbose,
).flatMap(
(_) => createFolder(
client,
temporaryFolder,
verbose: verbose,
).flatMap(
(_) {
final artifactFile = artifactFileToUpload.split('/').last;
return uploadFile(
client,
'$temporaryFolder/$artifactFile',
artifactFileToUpload,
verbose: verbose,
).flatMap(
(_) => unzipArchive(
client,
'$temporaryFolder/$artifactFile',
temporaryFolder,
verbose: verbose,
).flatMap(
(_) => untarFile(
client,
'$temporaryFolder/$archive',
temporaryFolder,
verbose: verbose,
).flatMap(
(_) {
final folderSearchString =
deployableFolderParent != null ? "$deployableFolderParent/$folderToDeploy" : folderToDeploy;
return getFolderPath(
client,
folderSearchString,
location: temporaryFolder,
verbose: verbose,
).flatMap(
(folderPath) => moveArtifactToLocationWithCleanup(
client,
folderPath,
folderToDeploy,
deploymentLocation,
folderParentName: deployableFolderParent,
verbose: verbose,
)
.flatMap(
(_) => giveFolderTreeExecutePermission(client, deploymentLocation).flatMap(
(_) => deleteFolderIfExist(client, temporaryFolder),
),
)
.flatMap(
(_) => closeSSHClient(client).toTaskEither(),
),
);
},
),
),
);
},
),
),
),
),
),
);
return request;
}
TaskEither<SSHCommandError, Unit> moveArtifactToLocationWithCleanup(
SSHClient client,
String folderPath,
String folderName,
String location, {
String? folderParentName,
bool verbose = false,
}) {
final request = createFolderIfNotExist(client, location)
.flatMap((_) => deleteFolderIfExist(client, '$location/$folderName').flatMap((_) => moveFolder(client, folderPath, location)));
return request.mapLeft((e) {
_log(e.errorMessage);
return e;
});
}
/// File & Folder
TaskEither<SSHCommandError, bool> doesFolderExists(
SSHClient client,
String folder, {
bool verbose = false,
}) {
String command = 'test -d $folder';
_log('Testing for folder $folder');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
return commandResult.isSuccess;
},
(error, stackTrace) => DoesFolderExistError(
e: error as Exception,
s: stackTrace,
),
);
}
TaskEither<SSHCommandError, Unit> createFolder(
SSHClient client,
String folder, {
bool verbose = false,
}) {
String command = 'mkdir $folder';
_log('creating folder $folder');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
if (commandResult.isSuccess) return unit;
throw Exception();
},
(e, s) {
client.close();
if (e is SSHCommandError) return e;
return CreateFolderError(
e: e as Exception,
s: s,
);
},
);
}
TaskEither<SSHCommandError, Unit> deleteFolder(
SSHClient client,
String folder, {
bool verbose = false,
}) {
String command = 'rm -rf $folder';
_log('deleting folder $folder');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
if (commandResult.isSuccess) return unit;
throw DeleteFolderError();
},
(e, s) {
client.close();
if (e is SSHCommandError) return e;
return DeleteFolderError(
e: e as Exception,
s: s,
);
},
);
}
TaskEither<SSHCommandError, Unit> moveFolder(
SSHClient client,
String folder,
String targetLocation, {
bool verbose = false,
}) {
String command = 'mv $folder $targetLocation';
_log('Moving folder $folder to $targetLocation');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
if (commandResult.isSuccess) return unit;
throw Exception(utf8.decode(commandResult.result));
},
(e, s) {
if (e is SSHCommandError) return e;
return MoveFolderError(
e: e as Exception,
s: s,
);
},
);
}
TaskEither<SSHCommandError, Unit> createFolderIfNotExist(
SSHClient client,
String folder, {
bool verbose = false,
}) =>
doesFolderExists(client, folder).flatMap<Unit>(
(exist) {
if (!exist) return createFolder(client, folder);
_log('Folder already exist nothing to create');
return TaskEither.of(unit);
},
);
TaskEither<SSHCommandError, Unit> deleteFolderIfExist(
SSHClient client,
String folder, {
bool verbose = false,
}) =>
doesFolderExists(client, folder).flatMap<Unit>(
(exist) {
if (exist) return deleteFolder(client, folder);
_log('Folder does not exist nothing to delete');
return TaskEither.of(unit);
},
);
TaskEither<SSHCommandError, String> getFolderPath(
SSHClient client,
String target, {
String location = '.',
bool verbose = false,
}) {
final command = 'find $location -regex \'.*/$target\'';
_log('Looking for $target path in the new folder');
if (verbose) _logger.debug('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
final result = utf8.decode(commandResult.result).trim();
final split = result.split('\n');
if (result.isEmpty || split.length != 1) throw GetFolderPathError();
return split.first;
},
(error, stackTrace) {
client.close();
if (error is SSHCommandError) return error;
return GetFolderPathError(e: error as Exception, s: stackTrace);
},
);
}
TaskEither<SSHCommandError, Unit> giveFolderTreeExecutePermission(
SSHClient client,
String target, {
bool verbose = false,
}) {
String command = 'chmod +x -R $target';
_log('Giving execute permission to $target');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(() async {
final commandResult = await client.runWithCode(command);
_log('Permission grant ${commandResult.isSuccess ? "successful" : "unsuccessful"}');
if (commandResult.isSuccess) return unit;
throw GiveExecutionPermissionError();
}, (e, s) {
client.close();
if (e is SSHCommandError) return e;
return GiveExecutionPermissionError(e: e as Exception, s: s);
});
}
TaskEither<SSHCommandError, bool> doesFileExists(
SSHClient client,
String folder, {
bool verbose = false,
}) {
String command = 'test -f $folder';
_log('Testing for file $folder');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
if (verbose) _log('The file ${commandResult.isSuccess ? "" : "does not"} exist');
return commandResult.isSuccess;
},
(error, stackTrace) {
client.close();
if (error is SSHCommandError) return error;
return DoesFileExistError(
e: error as Exception,
s: stackTrace,
);
},
);
}
/// Archive
TaskEither<SSHCommandError, Unit> untarFile(
SSHClient client,
String target,
String? location, {
bool verbose = false,
}) {
final command = 'tar -xvzf $target ${location != null ? "-C $location" : ""}';
_log('Untarring archive $target');
if (verbose) _logger.debug('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
if (commandResult.isSuccess) return unit;
throw UntarError();
},
(error, stackTrace) {
client.close();
if (error is SSHCommandError) return error;
return UntarError(e: error as Exception, s: stackTrace);
},
);
}
TaskEither<SSHCommandError, Unit> unzipArchive(
SSHClient client,
String target,
String? location, {
bool verbose = false,
}) {
final command = 'unzip $target ${location != null ? "-d $location" : ""}';
_log('Unzipping archive $target');
if (verbose) _logger.debug('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
_log('Archive unzipped');
if (verbose) _log('With result:\n ${utf8.decode(commandResult.result)}');
return unit;
},
(error, stackTrace) {
client.close();
if (error is SSHCommandError) return error;
return UnzipError(e: error as Exception, s: stackTrace);
},
);
}
/// SFTP
TaskEither<SSHCommandError, SftpClient> getSftp(
SSHClient client,
) {
return TaskEither.tryCatch(
() {
final sftp = client.sftp();
_log('sftp client created');
return sftp;
},
(e, s) {
client.close();
if (e is SSHCommandError) return e;
return GetSFTPClientError(e: e as Exception, s: s);
},
);
}
TaskEither<SSHCommandError, SftpFile> getSftpFile(
SftpClient client,
String destinationFile,
SftpFileOpenMode mode,
) {
return TaskEither.tryCatch(() {
final sftpFile = client.open(
destinationFile,
mode: mode,
);
_log('Created sftp file');
return sftpFile;
}, (e, s) {
client.close();
_log('Error while creating sftp file');
if (e is SSHCommandError) return e;
return GetSFTPFileError(e: e as Exception, s: s);
});
}
TaskEither<SSHCommandError, Unit> writeFile(SftpFile file, String sourceFilePath) => TaskEither.tryCatch(
() async {
await file.write(File(sourceFilePath).openRead().cast()).done;
_log('Wrote file on server');
return Future.value(unit);
},
(e, s) {
file.close();
if (e is SSHCommandError) return e;
return UnzipError(e: e as Exception, s: s);
},
);
TaskEither<SSHCommandError, Unit> uploadFile(
SSHClient client,
String destinationFile,
String sourceFile, {
bool verbose = false,
}) {
_log('Uploading $sourceFile');
final taskEitherRequest = getSftp(client).flatMap(
(sftp) => getSftpFile(
sftp,
destinationFile,
SftpFileOpenMode.create | SftpFileOpenMode.truncate | SftpFileOpenMode.write,
).flatMap(
(sftpFile) => writeFile(sftpFile, sourceFile),
),
);
return taskEitherRequest;
}
TaskEither<SSHCommandError, String> readSftpFile(
SftpFile sftpFile, {
bool verbose = false,
}) {
return TaskEither.tryCatch(() async {
return utf8.decode(await sftpFile.readBytes());
}, (e, s) {
sftpFile.close();
_log('Error while reading sftp file');
if (e is SSHCommandError) return e;
return ReadingSftpFileError(e: e as Exception, s: s);
});
}
TaskEither<SSHCommandError, Unit> printSftpFile(SSHClient client, String filePath) {
return getSftp(client).flatMap(
(sftp) => getSftpFile(
sftp,
filePath,
SftpFileOpenMode.read,
).flatMap(
(file) => readSftpFile(file).flatMap(
(fileContent) {
_log('Printing file: $filePath\n$fileContent');
return TaskEither.of(unit);
},
),
),
);
}
/// SSH Client
TaskEither<SSHCommandError, SSHClient> createSSHClient(
String serverIp,
int sshPort,
String user,
) =>
TaskEither.tryCatch(
() async {
_log('Initiating ssh connection with: $user@$serverIp:$sshPort');
final client = SSHClient(
await SSHSocket.connect(serverIp, sshPort),
username: user,
);
_log('Client connected');
return client;
},
(error, stackTrace) => SSHClientCreationError(
e: error as Exception,
s: stackTrace,
),
);
Either<SSHCommandError, Unit> closeSSHClient(SSHClient client, {bool verbose = false}) => Either.tryCatch(
() {
_log('closing ssh connection');
client.close();
_log('Ssh connection closed');
return unit;
},
(error, stackTrace) => SSHClientCloseError(
e: error as Exception,
s: stackTrace,
),
);
/// SystemD Service
TaskEither<SSHCommandError, Unit> createCustomService(
SSHClient client,
String serviceName, {
bool verbose = false,
}) {
String command = 'cp /lib/systemd/system/$serviceNameWithoutUser.service /etc/systemd/system/$serviceName.service';
// String command =
// 'env SYSTEMD_EDITOR=tee systemctl edit --full $serviceName.service < /lib/systemd/system/$serviceNameWithoutUser.service';
_log('Creating custom service: $serviceName');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(() async {
final commandResult = await client.runWithCode(command);
if (verbose) _log('The custom service has ${commandResult.isSuccess ? "" : "not"} been created');
if (commandResult.isSuccess) return unit;
throw CreateCustomServiceError();
}, (error, stackTrace) {
client.close();
if (error is SSHCommandError) return error;
return CreateCustomServiceError(
e: error as Exception,
s: stackTrace,
);
});
}
TaskEither<SSHCommandError, Unit> changeServiceWorkingDirectory(
SSHClient client,
String serviceFile,
String workingDirectory, {
bool verbose = false,
}) {
String command = 'sed -i "s%^WorkingDirectory=.*%WorkingDirectory=$workingDirectory%" "$serviceFile"';
_log('Replacing $serviceFile working directory by: $workingDirectory');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
if (verbose) _log('The working directory has ${commandResult.isSuccess ? "" : "not"} been replaced');
if (commandResult.isSuccess) return unit;
throw ChangeServiceWorkingDirectoryError();
},
(error, stackTrace) {
client.close();
if (error is SSHCommandError) return error;
return ChangeServiceWorkingDirectoryError(e: error as Exception, s: stackTrace);
},
);
}
TaskEither<SSHCommandError, Unit> changeServiceExecStart(
SSHClient client,
String serviceFile,
String execStar, {
bool verbose = true,
}) {
String command = 'sed -i "s%^ExecStart=.*%ExecStart=$execStar%" "$serviceFile"';
_log('Replacing $serviceFile exec start by: $execStar');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(() async {
final commandResult = await client.runWithCode(command);
if (verbose) _log('The exec start has ${commandResult.isSuccess ? "" : "not"} been replaced');
if (commandResult.isSuccess) return unit;
throw ChangeServiceStartExecError();
}, (e, s) {
client.close();
if (e is SSHCommandError) return e;
return ChangeServiceStartExecError(e: e as Exception, s: s);
});
}
TaskEither<SSHCommandError, Unit> reloadDaemon(
SSHClient client, {
bool verbose = false,
}) {
{
String command = 'systemctl daemon-reload';
_log('Reloading daemon');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(
() async {
final commandResult = await client.runWithCode(command);
if (verbose) _log('The daemon has ${commandResult.isSuccess ? "" : "not"} been reloaded');
if (commandResult.isSuccess) return unit;
throw ReloadDaemonError();
},
(e, s) {
client.close();
if (e is SSHCommandError) return e;
return ReloadDaemonError(e: e as Exception, s: s);
},
);
}
}
TaskEither<SSHCommandError, Unit> restartService(
SSHClient client,
String serviceName, {
bool verbose = false,
}) {
String command = 'systemctl restart $serviceName';
_log('Starting service: $serviceName');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(() async {
final commandResult = await client.runWithCode(command);
if (verbose) _log('The service has ${commandResult.isSuccess ? "" : "not"} been restarted');
if (commandResult.isSuccess) return unit;
throw ServiceRestartError();
}, (e, s) {
client.close();
if (e is SSHCommandError) return e;
return ServiceRestartError(e: e as Exception, s: s);
});
}
TaskEither<SSHCommandError, Unit> stopService(
SSHClient client,
String serviceName, {
bool verbose = false,
}) {
String command = 'systemctl stop $serviceName';
_log('Stopping service service: $serviceName');
if (verbose) _log('executing the command: $command');
return TaskEither.tryCatch(() async {
final commandResult = await client.runWithCode(command);
if (verbose) _log('The service has ${commandResult.isSuccess ? "" : "not"} been stoped');
if (commandResult.isSuccess) return unit;
throw ServiceStopError();
}, (e, s) {
client.close();
if (e is SSHCommandError) return e;
return ServiceStopError(e: e as Exception, s: s);
});
}
}
Of final implementation that only reuse the function provided by the abstract class:
class FrontendStrategy extends BaseSSHStrategy {
FrontendStrategy({
required super.logger,
});
@override
TaskEither<DeployStrategyError, Unit> execute() {
return depositArtifact().flatMap((_) => doSpoofService()).mapLeft((error) {
return ExecuteImplementationError(e: error.e, s: error.s);
});
}
}Finnally the code is executed like this:
final executeResult = await strategy.execute().run();
executeResult.fold(
(error) {
_logger.error('Error executing a strategy. With error: ${error.toString()}');
},
(_) {
_logger.info('Deploy strategy executed with success');
},
);