Mangrullo: a watchtower replacement

When you self-host things you often use Docker. And when you are responsible for things running with Docker how do you keep things updated?

One answer to this used to be watchtower, a daemon that updated your docker packages. BUT … it’s abandoned. So I wrote a slightly better replacement called Mangrullo

Crystal + Kemal + Docr + Other things as usual :-)

3 Likes

Noice, been kinda missing a watchtower replacement.

Sidenote: anyone knows of a Crystal Docker shard that allows for listening on events?

What kind of events?

Container start/stop/die primarily.

You can just run docker events --filter type=container --filter event=start and look at the output or something. Surely there is also a REST endpoint in the socket … maybe docr can be expanded to support it

This is what I do for Docker events. Works pretty well. I could extract it to a shard, but I haven’t yet.

require "http/client"
require "socket"
require "json"
require "uri"

socket_path = ENV.fetch("DOCKER_HOST", "/var/run/docker.sock").lchop("unix://")
log = Log.for("docker")

UNIXSocket.open socket_path do |socket|
  http = HTTP::Client.new(socket)
  filters = URI.encode_www_form(ARGV[0]? || "")

  log.notice &.emit "Listening for Docker events", socket: socket_path
  http.get("/events?#{filters}") do |response|
    unless response.status.success?
      STDERR.puts "Error: #{response.status} – #{response.body_io.gets_to_end}"
      exit 1
    end

    response.body_io.each_line do |line|
      next if line.blank?

      event = Event.from_json(line)
      log.info &.emit event.type,
        action: event.action,
        image: event.actor.attributes["image"]?.to_s

      # Do something with the event
    end
  end
end

struct Event
  include JSON::Serializable

  @[JSON::Field(key: "Type")]
  getter type : String
  @[JSON::Field(key: "Action")]
  getter action : String
  @[JSON::Field(key: "Actor")]
  getter actor : Actor
  getter scope : String
  @[JSON::Field(key: "timeNano", converter: Time::EpochNanosConverter)]
  getter time : Time

  struct Actor
    include JSON::Serializable

    @[JSON::Field(key: "ID")]
    getter id : String
    @[JSON::Field(key: "Attributes")]
    getter attributes : Hash(String, String)
  end
end

module Time::EpochNanosConverter
  extend self

  def from_json(json : JSON::PullParser)
    Time::UNIX_EPOCH + json.read_int.nanoseconds
  end
end

Just saying: there are Docker shards, so you can interact with a Docker server directly (though HTTP, SSH, …).

I patched an existing one a few years ago to automatically generate an API from the OpenAPI documentation for example:

Yeah, I use marghidanu/docr in this project

Great suggestions, certainly something to throw together a proof of concept.

I’ve developed an aversion to client generators. They cover the whole API and you end up with humongous amount of generated code where you end up using 5%, and you’re not quite sure which 5%. So you end up regenerating it at each new version, check that nothing broke and just commit the whole shebang and pollute your git log with unnecessary changes.

I have a little dream about a generator you tell what you need, and it just generates a very simple client that provides what you’re actually using.

1 Like