Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Nelson: Functional programming in system design

Nelson: Functional programming in system design

As functional programmers we work hard to keep things immutable and referentially transparent. However, these noble pursuits often stop with our text editors, the resulting code flung over the wall to be built and deployed in mutable, imperative systems. In this talk we will take a look at Nelson, a deployment orchestration system that applies functional programming not only in its implementation, but also in its system behavior in managing the messy world of deployment infrastructure. Where Free algebras and streams allow the code to be easily extended to support different source repositories, schedulers, and health checkers, Nelson's strict stance on immutable deployments inform important system level decisions such as deployment workflows and service discovery. Having now been run at a couple of companies under different configurations, Nelson serves as a prime example of how functional programming is for the real world and can influence not just code, but systems as well.

Adelbert Chang

March 19, 2018
Tweet

More Decks by Adelbert Chang

Other Decks in Programming

Transcript

  1. Nelson
    Functional programming in system design
    Adelbert Chang
    @adelbertchang
    Target

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. B
    5.4.5
    D
    2.1.8
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    Mutable deployments
    E
    4.12.7

    View Slide

  8. B
    5.4.5
    D
    2.1.8
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    Mutable deployments
    E
    4.12.7
    D
    2.2.0

    View Slide

  9. B
    5.4.5
    D
    2.2.0
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    Mutable deployments
    E
    4.12.7

    View Slide

  10. B
    5.4.5
    D
    2.2.0
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    Mutable deployments
    E
    4.12.7

    View Slide

  11. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    A
    1.1.5
    E
    4.12.7
    D
    2.1.8
    B
    5.4.5

    View Slide

  12. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    A
    1.1.5
    E
    4.12.7
    D
    2.1.8
    B
    5.4.5
    D
    2.2.0

    View Slide

  13. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    A
    1.1.5
    E
    4.12.7
    B
    5.4.5
    D
    2.1.8
    D
    2.2.0

    View Slide

  14. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    A
    1.1.5
    E
    4.12.7
    B
    5.4.5
    D
    2.1.8
    D
    2.2.0
    E
    4.12.8

    View Slide

  15. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    A
    1.1.5
    B
    5.4.5
    D
    2.1.8
    D
    2.2.0
    E
    4.12.8

    View Slide

  16. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    A
    1.1.5
    B
    5.4.5
    D
    2.1.8
    D
    2.2.0
    E
    4.12.8
    B
    5.4.6

    View Slide

  17. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    A
    1.1.5
    B
    5.4.5
    D
    2.1.8
    D
    2.2.0
    E
    4.12.8
    B
    5.4.6
    A
    1.1.6

    View Slide

  18. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    B
    5.4.5
    D
    2.1.8
    D
    2.2.0
    E
    4.12.8
    B
    5.4.6
    A
    1.1.6

    View Slide

  19. F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    Immutable deployments
    D
    2.2.0
    E
    4.12.8
    B
    5.4.6
    A
    1.1.6

    View Slide

  20. Reify the call graph
    B
    5.4.5
    D
    2.1.8
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7

    View Slide

  21. Reify the call graph
    B
    5.4.5
    D
    2.1.8
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7

    View Slide

  22. Reify the call graph
    B
    5.4.5
    D
    2.1.8
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7

    View Slide

  23. Reify the call graph
    B
    5.4.5
    D
    2.1.8
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7

    View Slide

  24. Reify the call graph
    B
    5.4.5
    D
    2.1.8
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7

    View Slide

  25. Reify the call graph
    B
    5.4.5
    D
    2.1.8
    F
    1.2.3
    G
    1.0.9
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7

    View Slide

  26. View Slide

  27. View Slide

  28. Name and version

    View Slide

  29. Name and version
    Dependencies

    View Slide

  30. Nelson workflow

    View Slide

  31. Nelson workflow

    View Slide

  32. Nelson workflow

    View Slide

  33. Nelson workflow

    View Slide

  34. Nelson workflow

    View Slide

  35. Graph management: traffic shifting
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5

    View Slide

  36. Graph management: traffic shifting
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    C
    2.3.7

    View Slide

  37. Graph management: traffic shifting
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    C
    2.3.7
    B
    5.4.6

    View Slide

  38. Graph management: traffic shifting
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    C
    2.3.7
    B
    5.4.6
    A
    1.1.6

    View Slide

  39. Graph management: traffic shifting
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    C
    2.3.7

    View Slide

  40. Graph management: traffic shifting
    C
    2.3.6
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    C
    2.3.7

    View Slide

  41. Graph management: traffic shifting
    D
    2.1.8
    C
    2.3.6
    A
    1.1.5
    B
    5.4.5
    C
    2.3.7
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9

    View Slide

  42. Graph management: traffic shifting
    D
    2.1.8
    A
    1.1.5
    B
    5.4.5
    C
    2.3.7
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9

    View Slide

  43. Graph management: pruning
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    C
    2.3.7

    View Slide

  44. Graph management: pruning
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    C
    2.3.7

    View Slide

  45. Graph management: pruning
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    F
    1.3.0
    C
    2.3.7

    View Slide

  46. Graph management: pruning
    A
    1.1.5
    E
    4.12.7
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7

    View Slide

  47. Graph management: pruning
    A
    1.1.5
    F
    1.2.3
    G
    1.0.9
    D
    2.1.8
    B
    5.4.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7

    View Slide

  48. Graph management: pruning
    A
    1.1.5
    D
    2.1.8
    B
    5.4.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7

    View Slide

  49. D
    2.1.8
    A
    1.1.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7
    B
    5.4.5
    Graph management: pruning

    View Slide

  50. D
    2.1.8
    A
    1.1.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7
    B
    5.4.5
    Graph management: pruning
    B
    5.4.6

    View Slide

  51. D
    2.1.8
    A
    1.1.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7
    B
    5.4.5
    Graph management: pruning
    B
    5.4.6

    View Slide

  52. D
    2.1.8
    A
    1.1.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7
    B
    5.4.5
    Graph management: pruning
    B
    5.4.6

    View Slide

  53. D
    2.1.8
    A
    1.1.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7
    Graph management: pruning
    B
    5.4.6

    View Slide

  54. D
    2.1.8
    A
    1.1.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7
    Graph management: pruning
    B
    5.4.6

    View Slide

  55. D
    2.1.8
    A
    1.1.5
    F
    1.3.0
    E
    4.12.8
    C
    2.3.7
    Job X
    1.8.5
    Graph management: pruning
    B
    5.4.6

    View Slide

  56. Time

    View Slide

  57. Time
    retain-latest

    View Slide

  58. X
    3.2.0
    Time
    retain-latest

    View Slide

  59. X
    3.2.0
    X
    3.2.1
    Time
    retain-latest

    View Slide

  60. X
    3.2.0
    X
    3.2.1
    Time
    retain-latest

    View Slide

  61. X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    Time
    retain-latest

    View Slide

  62. X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    Time
    retain-latest

    View Slide

  63. X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    Time
    retain-latest

    View Slide

  64. X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    Time
    retain-latest

    View Slide

  65. X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    X
    4.1.0
    Time
    retain-latest

    View Slide

  66. X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    X
    4.1.0
    Time
    retain-latest

    View Slide

  67. X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    X
    4.1.0
    Time
    retain-latest
    X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    X
    4.1.0
    retain-latest-two-major

    View Slide

  68. X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    X
    4.1.0
    Time
    retain-latest
    X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    X
    4.1.0
    retain-latest-two-major
    X
    3.2.0
    X
    3.2.1
    X
    3.3.0
    X
    4.0.0
    X
    4.1.0
    retain-latest-two-feature

    View Slide

  69. D
    2.1.8
    A
    1.1.5
    F
    1.2.4
    E
    4.12.8
    C
    2.3.7
    Job X
    1.8.5
    Graph management: pruning
    B
    5.4.6

    View Slide

  70. D
    2.1.8
    A
    1.1.5
    F
    1.2.4
    E
    4.12.8
    C
    2.3.7
    Job X
    1.8.5
    Job X
    1.9.0
    Graph management: pruning
    B
    5.4.6

    View Slide

  71. D
    2.1.8
    A
    1.1.5
    F
    1.2.4
    E
    4.12.8
    C
    2.3.7
    Job X
    1.8.5
    Job X
    1.9.0
    Job X
    1.8.6
    Graph management: pruning
    B
    5.4.6

    View Slide

  72. D
    2.1.8
    A
    1.1.5
    F
    1.2.4
    E
    4.12.8
    C
    2.3.7
    Job X
    1.9.0
    Job X
    1.8.6
    Graph management: pruning
    B
    5.4.6

    View Slide

  73. D
    2.1.8
    A
    1.1.5
    F
    1.2.4
    E
    4.12.8
    C
    2.3.7
    Job X
    1.9.0
    Job X
    1.8.6
    Graph management: pruning
    B
    5.4.6

    View Slide

  74. D
    2.1.8
    A
    1.1.5
    F
    1.2.4
    E
    4.12.8
    C
    2.3.7
    Job X
    1.9.0
    Job X
    1.8.6
    Job X
    1.9.1
    Graph management: pruning
    B
    5.4.6

    View Slide

  75. D
    2.1.8
    A
    1.1.5
    F
    1.2.4
    E
    4.12.8
    C
    2.3.7
    Job X
    1.8.6
    Job X
    1.9.1
    Graph management: pruning
    B
    5.4.6

    View Slide

  76. View Slide

  77. for {
    _ <- status(id, Pending, "workflow about to start")
    i <- dockerOps(id, unit, dc.docker.registry)
    Launch workflow

    View Slide

  78. for {
    _ <- status(id, Pending, "workflow about to start")
    i <- dockerOps(id, unit, dc.docker.registry)
    _ <- status(id, Deploying, s"writing alert definitions to ${dc.name}'s consul")
    _ <- writeAlertsToConsul(sn, ns.name, p.name, unit, p.environment.alertOptOuts)
    Launch workflow

    View Slide

  79. for {
    _ <- status(id, Pending, "workflow about to start")
    i <- dockerOps(id, unit, dc.docker.registry)
    _ <- status(id, Deploying, s"writing alert definitions to ${dc.name}'s consul")
    _ <- writeAlertsToConsul(sn, ns.name, p.name, unit, p.environment.alertOptOuts)
    Launch workflow
    _ <- logToFile(...)
    _ <- writePolicyToVault(cfg = dc.policy, sn = sn, ns = ns.name, rs = rs)

    View Slide

  80. for {
    _ <- status(id, Pending, "workflow about to start")
    i <- dockerOps(id, unit, dc.docker.registry)
    _ <- status(id, Deploying, s"writing alert definitions to ${dc.name}'s consul")
    _ <- writeAlertsToConsul(sn, ns.name, p.name, unit, p.environment.alertOptOuts)
    _ <- logToFile(...)
    _ <- writeDiscoveryToConsul(id, sn, ns.name, dc)
    Launch workflow
    _ <- logToFile(...)
    _ <- writePolicyToVault(cfg = dc.policy, sn = sn, ns = ns.name, rs = rs)

    View Slide

  81. for {
    _ <- status(id, Pending, "workflow about to start")
    i <- dockerOps(id, unit, dc.docker.registry)
    _ <- status(id, Deploying, s"writing alert definitions to ${dc.name}'s consul")
    _ <- writeAlertsToConsul(sn, ns.name, p.name, unit, p.environment.alertOptOuts)
    _ <- logToFile(...)
    _ <- writeDiscoveryToConsul(id, sn, ns.name, dc)
    _ <- getTrafficShift.cata(...)
    _ <- logToFile(...)
    Launch workflow
    _ <- logToFile(...)
    _ <- writePolicyToVault(cfg = dc.policy, sn = sn, ns = ns.name, rs = rs)

    View Slide

  82. for {
    _ <- status(id, Pending, "workflow about to start")
    i <- dockerOps(id, unit, dc.docker.registry)
    _ <- status(id, Deploying, s"writing alert definitions to ${dc.name}'s consul")
    _ <- writeAlertsToConsul(sn, ns.name, p.name, unit, p.environment.alertOptOuts)
    _ <- logToFile(...)
    _ <- writeDiscoveryToConsul(id, sn, ns.name, dc)
    _ <- getTrafficShift.cata(...)
    _ <- logToFile(...)
    Launch workflow
    _ <- logToFile(...)
    _ <- writePolicyToVault(cfg = dc.policy, sn = sn, ns = ns.name, rs = rs)
    l <- launch(i, dc, ns.name, vunit, p, hash)
    _ <- debug(s"response from scheduler $l")

    View Slide

  83. for {
    _ <- status(id, Pending, "workflow about to start")
    i <- dockerOps(id, unit, dc.docker.registry)
    _ <- status(id, Deploying, s"writing alert definitions to ${dc.name}'s consul")
    _ <- writeAlertsToConsul(sn, ns.name, p.name, unit, p.environment.alertOptOuts)
    _ <- logToFile(...)
    _ <- writeDiscoveryToConsul(id, sn, ns.name, dc)
    _ <- getTrafficShift.cata(...)
    _ <- logToFile(...)
    _ <- status(id, getStatus(unit, p), "======> workflow completed <======")
    } yield ()
    Launch workflow
    _ <- logToFile(...)
    _ <- writePolicyToVault(cfg = dc.policy, sn = sn, ns = ns.name, rs = rs)
    l <- launch(i, dc, ns.name, vunit, p, hash)
    _ <- debug(s"response from scheduler $l")

    View Slide

  84. logToFile(d.id, s"removing policy from vault: ${vaultLoggingFields(...)}") >>
    deletePolicyFromVault(d.stackName, ns.name) >>
    Delete workflow

    View Slide

  85. logToFile(d.id, s"removing policy from vault: ${vaultLoggingFields(...)}") >>
    deletePolicyFromVault(d.stackName, ns.name) >>
    Delete workflow
    logToFile(d.id, s"removing alerts from consul ${alerts.alertingKey(sn)}") >>
    deleteAlertsFromConsul(d.stackName) >>

    View Slide

  86. logToFile(d.id, s"removing policy from vault: ${vaultLoggingFields(...)}") >>
    deletePolicyFromVault(d.stackName, ns.name) >>
    Delete workflow
    logToFile(d.id, ...) >>
    deleteDiscoveryInfoFromConsul(sn) >>
    logToFile(d.id, s"removing alerts from consul ${alerts.alertingKey(sn)}") >>
    deleteAlertsFromConsul(d.stackName) >>

    View Slide

  87. logToFile(d.id, s"removing policy from vault: ${vaultLoggingFields(...)}") >>
    deletePolicyFromVault(d.stackName, ns.name) >>
    Delete workflow
    logToFile(d.id, ...) >>
    deleteDiscoveryInfoFromConsul(sn) >>
    logToFile(d.id, s"removing alerts from consul ${alerts.alertingKey(sn)}") >>
    deleteAlertsFromConsul(d.stackName) >>
    logToFile(d.id, s"instructing ${dc.name}'s scheduler to decommission ${sn}") >>
    delete(dc,d) >>

    View Slide

  88. logToFile(d.id, s"removing policy from vault: ${vaultLoggingFields(...)}") >>
    deletePolicyFromVault(d.stackName, ns.name) >>
    Delete workflow
    logToFile(d.id, ...) >>
    deleteDiscoveryInfoFromConsul(sn) >>
    logToFile(d.id, s"removing alerts from consul ${alerts.alertingKey(sn)}") >>
    deleteAlertsFromConsul(d.stackName) >>
    logToFile(d.id, s"instructing ${dc.name}'s scheduler to decommission ${sn}") >>
    delete(dc,d) >>
    status(d.id, Terminated, s"Decommissioning deployment ${sn} in ${dc.name}")

    View Slide

  89. runBackgroundJob("auditor", cfg.auditor.process(cfg.storage))
    Background processes

    View Slide

  90. runBackgroundJob("auditor", cfg.auditor.process(cfg.storage))
    runBackgroundJob("pipeline_processor",
    Process.eval(Pipeline.task(cfg)(Pipeline.sinks.runAction(cfg))))
    Background processes

    View Slide

  91. runBackgroundJob("auditor", cfg.auditor.process(cfg.storage))
    runBackgroundJob("pipeline_processor",
    Process.eval(Pipeline.task(cfg)(Pipeline.sinks.runAction(cfg))))
    runBackgroundJob("workflow_logger", cfg.workflowLogger.process)
    Background processes

    View Slide

  92. runBackgroundJob("auditor", cfg.auditor.process(cfg.storage))
    runBackgroundJob("pipeline_processor",
    Process.eval(Pipeline.task(cfg)(Pipeline.sinks.runAction(cfg))))
    runBackgroundJob("workflow_logger", cfg.workflowLogger.process)
    runBackgroundJob("routing_cron",
    routing.cron.consulRefresh(cfg) to Http4sConsul.consulSink)
    Background processes

    View Slide

  93. runBackgroundJob("auditor", cfg.auditor.process(cfg.storage))
    runBackgroundJob("pipeline_processor",
    Process.eval(Pipeline.task(cfg)(Pipeline.sinks.runAction(cfg))))
    runBackgroundJob("workflow_logger", cfg.workflowLogger.process)
    runBackgroundJob("routing_cron",
    routing.cron.consulRefresh(cfg) to Http4sConsul.consulSink)
    runBackgroundJob("cleanup_pipeline", cleanup.CleanupCron.pipeline(cfg))
    Background processes

    View Slide

  94. runBackgroundJob("auditor", cfg.auditor.process(cfg.storage))
    runBackgroundJob("pipeline_processor",
    Process.eval(Pipeline.task(cfg)(Pipeline.sinks.runAction(cfg))))
    runBackgroundJob("workflow_logger", cfg.workflowLogger.process)
    runBackgroundJob("routing_cron",
    routing.cron.consulRefresh(cfg) to Http4sConsul.consulSink)
    runBackgroundJob("cleanup_pipeline", cleanup.CleanupCron.pipeline(cfg))
    runBackgroundJob("sweeper", cleanup.Sweeper.process(cfg))
    Background processes

    View Slide

  95. runBackgroundJob("auditor", cfg.auditor.process(cfg.storage))
    runBackgroundJob("pipeline_processor",
    Process.eval(Pipeline.task(cfg)(Pipeline.sinks.runAction(cfg))))
    runBackgroundJob("workflow_logger", cfg.workflowLogger.process)
    runBackgroundJob("routing_cron",
    routing.cron.consulRefresh(cfg) to Http4sConsul.consulSink)
    runBackgroundJob("cleanup_pipeline", cleanup.CleanupCron.pipeline(cfg))
    runBackgroundJob("sweeper", cleanup.Sweeper.process(cfg))
    runBackgroundJob("deployment_monitor", DeploymentMonitor.loop(cfg))
    Background processes

    View Slide

  96. type Op0[A] = Coproduct[DockerOp, ConsulOp, A]
    type Op1[A] = Coproduct[LoggingOp, Op0,A]
    type Op2[A] = Coproduct[StoreOp, Op1, A]
    type Op3[A] = Coproduct[WorkflowControlOp, Op2,A]
    type Op4[A] = Coproduct[Vault, Op3, A]
    type WorkflowOp[A] = Coproduct[SchedulerOp, Op4, A]
    type WorkflowF[A] = Free.FreeC[WorkflowOp, A]
    Workflow algebra

    View Slide

  97. final case class Interpreters(
    scheduler: SchedulerOp ~> Task,
    consul: ConsulOp ~> Task,
    vault: Vault ~> Task,
    storage: StoreOp ~> Task,
    logger: LoggingOp ~> Task,
    docker: DockerOp ~> Task,
    control: WorkflowControlOp ~> Task,
    health: HealthCheckOp ~> Task
    )
    Workflow interpreter(s)

    View Slide

  98. View Slide

  99. View Slide

  100. View Slide

  101. View Slide

  102. View Slide

  103. View Slide

  104. View Slide

  105. View Slide

  106. View Slide

  107. View Slide

  108. https://getnelson.github.io/
    https://github.com/getnelson/
    https://gitter.im/getnelson/nelson

    View Slide