Skip to content

Commit

Permalink
refactoring (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
vlmiroshnikov authored Dec 23, 2024
1 parent f098026 commit f2a2157
Show file tree
Hide file tree
Showing 19 changed files with 412 additions and 201 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
scala: [3.3.1]
scala: [ 3.6.2 ]
java: [openjdk@1.17.0]
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -43,7 +43,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
scala: [3.3.1]
scala: [ 3.6.2 ]
java: [openjdk@1.17.0]
runs-on: ${{ matrix.os }}
steps:
Expand Down
15 changes: 13 additions & 2 deletions alpasso/src/main/scala/alpasso/cli/ArgParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ enum RepoOps:
case Init(path: Option[Path], cypher: CypherAlg)
case List
case Switch(index: Int)
case Log

enum Action:
case Repo(ops: RepoOps)
case New(name: SecretName, secret: Option[SecretPayload], meta: Option[SecretMetadata])
case Patch(name: SecretName, payload: Option[SecretPayload], meta: Option[SecretMetadata])
case Filter(where: SecretFilter, format: OutputFormat)
case Remove(name: SecretName)

object ArgParser:

Expand All @@ -41,7 +43,11 @@ object ArgParser:
path.map(RepoOps.Switch.apply)
}

(init orElse list orElse switch).map(Action.Repo.apply)
val log = Opts.subcommand("log", "log repository") {
Opts.apply(RepoOps.Log)
}

(init orElse list orElse switch orElse log).map(Action.Repo.apply)
}

val add: Opts[Action] = Opts.subcommand("new", "Add new secret") {
Expand Down Expand Up @@ -77,8 +83,13 @@ object ArgParser:
)
}

val remove: Opts[Action] = Opts.subcommand("rm", "Remove secret") {
val name = Opts.argument("name").mapValidated(SecretName.of)
name.map(Action.Remove(_))
}

val command: Command[Action] =
Command("alpasso", "header", true)(repos orElse add orElse list orElse patch)
Command("alpasso", "header", true)(repos orElse add orElse remove orElse list orElse patch)

@main
def parse(): Unit = {
Expand Down
24 changes: 15 additions & 9 deletions alpasso/src/main/scala/alpasso/cli/CliApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import cats.syntax.all.*
import alpasso.cli
import alpasso.cmdline.*
import alpasso.cmdline.view.*
import alpasso.cmdline.view.SessionTableView.given
import alpasso.cmdline.view.SessionView.given
import alpasso.cmdline.view.given
import alpasso.common.*
import alpasso.common.syntax.*
import alpasso.core.model.*
Expand All @@ -37,17 +37,19 @@ object CliApp extends IOApp:

def handle[T: Show](result: Result[T]): IO[ExitCode] =
result match
case Left(e) => IO.println(s"Error: $e").as(ExitCode.Error)
case Left(e) => IO.println(e.into().show).as(ExitCode.Error)
case Right(r) => IO.println(r.show).as(ExitCode.Success)

def provideCommand[A](f: Command[IO] => IO[Result[A]]): IO[Result[A]] =
def provideConfig[A](f: RepositoryConfiguration => IO[Result[A]]): IO[Result[A]] =
(for
session <- EitherT.fromOptionF(smgr.current(), Err.UseSwitchCommand)
cfg <- rmr.read(session.path).liftE[Err]
configuration = RepositoryConfiguration(session.path, cfg.version, cfg.cryptoAlg)
result <- f(Command.make[IO](configuration)).liftE[Err]
cfg <- rmr.read(session.path).liftE[Err]
result <- f(RepositoryConfiguration(session.path, cfg.version, cfg.cryptoAlg)).liftE[Err]
yield result).value

def provideCommand[A](f: Command[IO] => IO[Result[A]]): IO[Result[A]] =
provideConfig(config => f(Command.make[IO](config)))

ArgParser.command.parse(args) match
case Left(help) =>
handle(Err.CommandSyntaxError(help.toString).asLeft[Unit])
Expand All @@ -59,6 +61,7 @@ object CliApp extends IOApp:
(bootstrap[IO](path, SemVer.zero, cypher) <* smgr.setup(Session(path))) >>= handle

case RepoOps.List => smgr.listAll().map(_.into().asRight[Err]) >>= handle
case RepoOps.Log => provideConfig(historyLog) >>= handle
case RepoOps.Switch(sel) =>
val switch = OptionT(smgr.listAll().map(_.zipWithIndex.find((_, idx) => idx == sel)))
.cataF(
Expand All @@ -70,12 +73,15 @@ object CliApp extends IOApp:
case Right(Action.New(sn, sp, sm)) =>
provideCommand(_.create(sn, sp.getOrElse(SecretPayload.empty), sm)) >>= handle

case Right(Action.Filter(where, OutputFormat.Tree)) =>
provideCommand(_.filter(where)) >>= handle
case Right(Action.Remove(sn)) =>
provideCommand(_.remove(sn)) >>= handle

case Right(Action.Patch(sn, spOpt, smOpt)) =>
provideCommand(_.patch(sn, spOpt, smOpt)) >>= handle

case Right(Action.Filter(where, OutputFormat.Tree)) =>
provideCommand(_.filter(where)) >>= handle

case Right(Action.Filter(where, OutputFormat.Table)) =>
val res = provideCommand(_.filter(where))
val buildTableView = res
Expand All @@ -85,7 +91,7 @@ object CliApp extends IOApp:
val v = root.foldLeft(List.empty[SecretView]):
case (agg, Branch.Empty(_)) => agg
case (agg, Branch.Solid(_, a)) => agg :+ a
TableView(v.mapWithIndex((s, i) => TableRowView(i, s)))
TableView(v)
}
.value
.value
Expand Down
100 changes: 57 additions & 43 deletions alpasso/src/main/scala/alpasso/cmdline/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,52 @@ import cats.data.*
import cats.effect.*
import cats.syntax.all.*

import alpasso.cmdline.view.*
import alpasso.cmdline.view.{*, given}
import alpasso.common.syntax.*
import alpasso.common.{ Logger, Result, SemVer }
import alpasso.common.{Logger, RawPackage, Result, SemVer}
import alpasso.core.model.*
import alpasso.service.cypher.*
import alpasso.service.fs.*
import alpasso.service.fs.model.{ Branch, * }
import alpasso.service.git.*
import alpasso.service.git.{GitError, GitRepo}

import glass.*

enum Err:
case AlreadyExists(name: SecretName)
case SecretRepoErr(err: RepositoryErr)

case StorageNotInitialized(path: Path)
case InconsistentStorage(reason: String)
case StorageCorrupted(path: Path)
case SecretNotFound(name: SecretName)
case CypherErr

case InternalErr
case CommandSyntaxError(help: String)
case UseSwitchCommand
case RepositoryProvisionErr(err: ProvisionErr)

object Err:
given Upcast[Err, GitError] = fromGitError
given Upcast[Err, RepoMetaErr] = _ => Err.InternalErr
given Upcast[Err, ProvisionErr] = _ => Err.InternalErr
given Upcast[Err, CypherError] = _ => Err.CypherErr

private def fromGitError(ge: GitError): Err =
ge match
case GitError.RepositoryNotFound(path) => Err.StorageNotInitialized(path)
case GitError.RepositoryIsDirty => Err.InconsistentStorage("repo is dirty")
case GitError.UnexpectedError => Err.InternalErr
given Upcast[Err, RepositoryErr] = Err.SecretRepoErr(_)

given Upcast[Err, RepoMetaErr] = _ => Err.InternalErr

given Upcast[Err, ProvisionErr] = e => Err.RepositoryProvisionErr(e)

given Upcast[Err, CypherError] = e => Err.SecretRepoErr(e.upcast)
end Err

def bootstrap[F[_]: Sync: Logger](repoDir: Path, version: SemVer, cypher: CypherAlg): F[Result[StorageView]] =
val provisioner = RepositoryProvisioner.make(repoDir)
val config = RepositoryMetaConfig(version, cypher)
provisioner.provision(config).liftE[Err].map(_ => StorageView(repoDir)).value
provisioner.provision(config).liftE[Err].map(_ => StorageView(repoDir, cypher)).value

def historyLog[F[_] : Sync](configuration: RepositoryConfiguration): F[Result[HistoryLogView]] =
GitRepo.openExists(configuration.repoDir).use { git =>
import RepositoryErr.*
given Upcast[Err, GitError] =
ge => summon[Upcast[Err, RepositoryErr]].upcast(fromGitError(ge)) // todo fix it

git.history().nested.map(v => HistoryLogView.from(v.commits)).value.liftE[Err].value
}

trait Command[F[_]]:

Expand All @@ -59,6 +66,7 @@ trait Command[F[_]]:
payload: Option[SecretPayload],
meta: Option[SecretMetadata]): F[Result[SecretView]]

def remove(name: SecretName): F[Result[SecretView]]
def filter(filter: SecretFilter): F[Result[Option[Node[Branch[SecretView]]]]]

object Command:
Expand All @@ -78,40 +86,45 @@ object Command:
extends Command[F]:

override def filter(filter: SecretFilter): F[Result[Option[Node[Branch[SecretView]]]]] =
def predicate(s: SecretPacket[(RawSecretData, RawMetadata)]): Boolean =
def predicate(p: RawPackage): Boolean =
filter match
case SecretFilter.Grep(pattern) => s.name.contains(pattern)
case SecretFilter.Grep(pattern) => p.name.contains(pattern)
case SecretFilter.Empty => true

def load(s: SecretPacket[RawStoreLocations]) =
def load(s: SecretPackage[RawStoreLocations]) =
reader.loadFully(s).liftE[Err]

(for
val result = for
rawTree <- reader.walkTree.liftE[Err]
tree <- rawTree.traverse(branch => branch.traverse(load))
yield cutTree(tree, predicate)
.map(
_.traverse(b =>
Id(
b.map(sm =>
SecretView(sm.name,
MetadataView(sm.payload._2.into()).some,
new String(sm.payload._1.byteArray).some
)
)
)
)
)).value
tree <- rawTree.traverse(_.traverse(load))
yield cutTree(tree, predicate).map(_.traverse(b => Id(b.map(_.into()))))

result.value

override def create(
name: SecretName,
payload: SecretPayload,
meta: Option[SecretMetadata]): F[Result[SecretView]] =
val rmd = meta.map(RawMetadata.from).getOrElse(RawMetadata.empty)
val result =
for
data <- cs.encrypt(payload.rawData).liftE[Err]
locations <- mutator.create(name, RawSecretData.from(data), RawMetadata.empty).liftE[Err]
yield SecretView(locations.name, meta.map(MetadataView(_)))
locations <- mutator.create(name, RawSecretData.from(data), rmd).liftE[Err]
yield SecretView(name, None, meta)

result.value

override def remove(name: SecretName): F[Result[SecretView]] =
val result = for
catalog <- reader.walkTree.liftE[Err]
exists <- catalog
.find(_.fold(false, _.name == name))
.flatMap(_.toOption)
.toRight(RepositoryErr.NotFound(name))
.pure[F]
.liftE[Err]
_ <- mutator.remove(exists.name).liftE[Err]
yield SecretView(name, None, None)

result.value

Expand All @@ -125,17 +138,18 @@ object Command:
exists <- catalog
.find(_.fold(false, _.name == name))
.flatMap(_.toOption)
.toRight(Err.SecretNotFound(name))
.toEitherT
.toRight(RepositoryErr.NotFound(name))
.pure[F]
.liftE[Err]

toUpdate <- reader.loadFully(exists).liftE[Err]

rsd = payload.map(_.rawData).getOrElse(toUpdate.payload._1.byteArray)
metadata = meta.getOrElse(toUpdate.payload._2.into())
rsd = payload.map(_.rawData).getOrElse(toUpdate.payload._1.byteArray)
rmd = meta.map(RawMetadata.from).getOrElse(toUpdate.payload._2)

sec <- cs.encrypt(rsd).liftE[Err]
locations <- mutator.update(name, RawSecretData.from(sec), RawMetadata.empty).liftE[Err]
yield SecretView(locations.name, None)
locations <- mutator.update(name, RawSecretData.from(sec), rmd).liftE[Err]
yield SecretView(name, None, None)

result.value
end Command
30 changes: 29 additions & 1 deletion alpasso/src/main/scala/alpasso/cmdline/view/ErrorView.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
package alpasso.cmdline.view

case class ErrorView(code: String, explain: Option[String])
import scala.Console.*

import cats.*
import cats.syntax.show.*

import alpasso.cmdline.*
import alpasso.common.Converter
import alpasso.core.model.SecretName.given
import alpasso.service.fs.{ ProvisionErr, RepositoryErr }

case class ErrorView(error: String, suggest: Option[String] = None)

object ErrorView:

given Show[ErrorView] = Show.show(s => s"${s.error} ${s.suggest.getOrElse("")}")

given Converter[Err, ErrorView] =
case Err.RepositoryProvisionErr(ProvisionErr.AlreadyExists(path)) =>
ErrorView(s"${RED}Repository at ${RESET}${BLUE}[${path.toString}]${RESET} ${RED}is already exists${RESET}")

case Err.SecretRepoErr(inner) =>
inner match
case RepositoryErr.AlreadyExists(name) =>
ErrorView(s"${RED}Secret $RESET $BLUE[${name.show}]$RESET ${RED}is already exists$RESET")
case RepositoryErr.NotFound(name) =>
ErrorView(s"${RED}Secret ${RESET}${BLUE}[${name.show}]${RESET} ${RED}not found${RESET}")
case ee => ErrorView(s"Undefined error: ${ee}")

case ee => ErrorView(s"Undefined error: ${ee}")
17 changes: 12 additions & 5 deletions alpasso/src/main/scala/alpasso/cmdline/view/SecretView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ package alpasso.cmdline.view
import cats.*
import cats.syntax.all.*

import alpasso.common.{Converter, RawPackage}
import alpasso.core.model.*
import alpasso.service.fs.model.given
import alpasso.service.fs.model.{RawMetadata, RawSecretData}

import Console.*

case class SecretView(
name: SecretName,
metadata: Option[MetadataView],
payload: Option[String] = None)
payload: Option[String] = None,
metadata: Option[SecretMetadata])

given Converter[RawPackage, SecretView] =
rp => SecretView(rp.name, new String(rp.payload._1.byteArray).some, rp.payload._2.into().some)

object SecretView:

given Show[SecretView] = Show.show(s =>
s" ${GREEN}${s.name}${RESET} ${BLUE_B} ${s.payload.getOrElse("******")}${RESET} ${s.metadata.show}"
)
given Show[SecretView] = Show.show { s =>
val tags = s.metadata.fold("")(_.asMap.map((k, v) => s"$k=$v").mkString(","))
s"${GREEN}${s.name}${RESET} $BLUE${s.payload.getOrElse("******")}$RESET $YELLOW$tags$RESET"
}
Loading

0 comments on commit f2a2157

Please sign in to comment.