Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code idiom review #159

Open
Driky opened this issue Nov 19, 2024 · 0 comments
Open

Code idiom review #159

Driky opened this issue Nov 19, 2024 · 0 comments

Comments

@Driky
Copy link

Driky commented Nov 19, 2024

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 doSpoofService is spoofService written in Do. depositArtifact would 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 Reader to 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');
    },
  );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant