git.delta.rocks / jrsonnet / refs/commits / 11412de2e73d

difftreelog

doc: add proper fleet logo

Lach2025-06-17parent: #b675486.patch.diff
in: trunk

3 files changed

modifiedREADME.adocdiffbeforeafterboth
before · README.adoc

fleet temporary logo generated with midjourney

An NixOS cluster deployment tool.

Advantages over existing configuration systems (NixOps/Morph)

  • Modules can configure multiple hosts at once (I.e for wireguard/kubernetes installation)

  • Secrets can be securely stored in Git (No one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.

  • Automatic rollback on deployment failure, which will work, as long as system is passing initrd stage (So still be carefull with root filesystem mount)

Flake example

{
  description = "My cluster configuration";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
    fleet = {
      url = "github:CertainLach/fleet";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    flake-parts.url = "github:hercules-ci/flake-parts";
    lanzaboote = {
      url = "github:nix-community/lanzaboote/v0.3.0";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
  outputs = inputs:
    inputs.flake-parts.lib.mkFlake {inherit inputs;} {
      imports = [inputs.fleet.flakeModules.default];

      perSystem = {
        inputs',
        pkgs,
        system,
        ...
      }: {
        formatter = pkgs.alejandra;
        devShells.default =
          pkgs.mkShell {packages = [inputs'.fleet.packages.fleet];};
      };

      # Single flake may contain multiple fleet configurations, default one is called... `default`
      fleetConfigurations.default = {
        # nixos option section of fleet config declares module, which is used for all configured nixos hosts.
        nixos = {
          imports = [inputs.lanzaboote.nixosModules.lanzaboote];

          # Make `nix shell nixpkgs#thing` use the same nixpkgs, as used to build the system.
          nix.registry.nixpkgs = {
            from = {
              id = "nixpkgs";
              type = "indirect";
            };
            flake = inputs.nixpkgs;
            exact = false;
          };
        };

        # Those modules are used to configure all the machines in cluster at the same time, good example of global modules
        # Is I.e wiring up the mesh VPN, or deploying kubernetes, or other things.
        #
        # Modules use the same semantics as standard nixos module system, they are just configuring all the hosts at once.
        imports = [
          ./wireguard
          # Multi-instancible modules example
          (import ./kubernetes {hosts = ["a" "b"];})
          (import ./kubernetes {hosts = ["c" "d"];})
        ];

        # Hosts attribute (may also be defined/extended using modules attribute) configures hosts...
        hosts.controlplane-1 = {
          # Every host has some system, for which the system configuration needs to be built
          system = "x86_64-linux";
          nixos = {
            # And nixos modules
            imports = [
              ./controlplane-1/hardware-configuration.nix
              ./controlplane-1/configuration.nix
            ];
            # Configuration may also be specified inline, as in any nixos config.
            services.ray = {
              gpus = 4;
              cpus = 128;
            };
          };
        };
      };
    };
}

Secret generator example

TODO

This section should into some kind of fleet documentation…​ But as there is none, it is just left here as-is.

Quickly run securely setup gitlab

{config, ...}: {
  secrets = let ownership = { owner = "gitlab"; group = "gitlab"; }; in {
    gitlab-initial-root = {
      generator = {mkPassword}: mkPassword {};
    } // ownership;
    gitlab-secret = {
      generator = {mkPassword}: mkPassword {};
    } // ownership;
    gitlab-otp = {
      generator = {mkPassword}: mkPassword {};
    } // ownership;
    gitlab-db = {
      generator = {mkPassword}: mkPassword {};
    } // ownership;
    gitlab-jws = {
      generator = {mkRsa}: mkRsa {};
    } // ownership;
  };
  services.gitlab = let secrets = config.secrets; in {
    enable = true;
    initialRootPasswordFile = secrets.gitlab-initial-root.secretPath;
    secrets = {
      secretFile = secrets.gitlab-secret.secretPath;
      otpFile = secrets.gitlab-otp.secretPath;
      dbFile = secrets.gitlab-db.secretPath;
      jwsFile = secrets.gitlab-jws.secretPath;
    };
  };
}

Securely initialize kubernetes secrets

In my homelab and clusters, I almost always have some sort of HSM, and to issue new kubernetes certs I directly connect to it. This setup should probably split into multiple steps, where I allow target machine to generate CSR, then copy it to the HSM machine, and then sign it there…​ But this is just the plan. I want to build ansible-like script execution in fleet for this kind of tasks.

{...}: {
  # First I define required secret generators:
  nixpkgs.overlays = [
    (final: prev: let
      lib = final.lib;
    in {
      readKubernetesCa = {impureOn}:
        final.mkImpureSecretGenerator ''
          cd ~/ca

          cert=kubernetes-intermediateCA.crt

          expires_at=$(openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")
          echo -n $expires_at > $out/expires_at

          cat $cert > $out/public
        ''
        impureOn;
      mkKubernetesCert = {
        subj,
        sans ? [],
        impureOn,
      }:
        final.mkImpureSecretGenerator ''
          cd ~/ca

          params=$(sudo mktemp)
          csr=$(sudo mktemp)
          cert=$(sudo mktemp)
          sudo openssl ecparam -genkey -name secp384r1 -out $params
          sudo openssl req -new -key $params \
            -subj "${lib.strings.concatStringsSep "" (lib.attrsets.mapAttrsToList (k: v: "/${k}=${v}") subj)}" \
            ${lib.optionalString (sans != []) "-addext \"subjectAltName = ${lib.strings.concatStringsSep "," sans}\""} \
            -out $csr
          sudo hsms x509 -req -days 365 -in $csr -CA kubernetes-intermediateCA.crt -CAkey "pkcs11:object=[CENSORED] Kubernetes Intermediate CA;type=private" -CAcreateserial -copy_extensions copy -out $cert

          expires_at=$(sudo openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")
          echo -n $expires_at > $out/expires_at

          sudo cat $params | encrypt > $out/secret
          sudo cat $cert > $out/public
        ''
        impureOn;
    })
  ];
  # Those secret generators are impure, thus they are run in system environment.
  # Probably there needs to be a dedicated user for that kind of tasks, but this is my current setup, don't judge.
  # I write a couple of scripts for executing openssl with HSM.
  environment.systemPackages = [
    pkgs.openssl.bin
    (pkgs.writeShellApplication {
      name = "hsms";
      text = ''
        set -eu
        export OPENSSL_CONF=${openssl-conf}
        # Yay, using secrets to generate secrets!
        HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})
        exec ${pkgs.openssl}/bin/openssl "$@" -keyform=engine -CAkeyform=engine -engine=pkcs11 -passin=pass:"$HSM_PIN"
      '';
    })
    (pkgs.writeShellApplication {
      name = "hsmt";
      text = ''
        set -eu
        HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})
        exec ${pkgs.opensc}/bin/pkcs11-tool -l --pin="$HSM_PIN" "$@"
      '';
    })
  ];
  # And finally, I have secrets, which are shared between machines.
  # Note that this example is somewhat wrong, as this goes not into the machine configuration, but to fleet configuration.
  sharedSecrets = {
    "ca.pem" = {
      # This is just the public key, no need to regenerate it to change owner list
      regenerateOnOwnerAdded = false;
      # For secret regeneration/reencryption, we need to specify which machines SHOULD have it.
      expectedOwners = ["controlplane-1" "controlplane-2" "worker-1" "worker-2"];
      generator = {readKubernetesCa}:
        readKubernetesCa {
          impureOn = "[CENSORED]";
        };
    };
    "kube-admin.pem" = {
      regenerateOnOwnerAdded = false;
      expectedOwners = ["cluster-admin"];
      generator = {mkKubernetesCert}:
        mkKubernetesCert {
          subj = {
            CN = "admin";
            O = "system:masters";
          };
          impureOn = "[CENSORED]";
        };
    };
    "kube-apiserver.pem" = {
      # This secret depends on machine SANS, so if owner list has been changed, then we need to regenerate it.
      # However, SANS dependency is in fact handled by secret seed, and secret is regenerated if the seed is changed...
      #
      # In this case regeneration is added as a half-assed security measure, as if apiserver is removed, we don't
      # want for it to be able to pretend like it is a valid server.
      #
      # However, certificate revokation is complicated in my setup, and I can't show it here.
      regenerateOnOwnerAdded = true;
      expectedOwners = ["controlplane-1" "controlplane-2"];
      generator = {mkKubernetesCert}:
        mkKubernetesCert {
          inherit sans;
          subj.CN = "kubernetes";
          impureOn = "[CENSORED]";
        };
    };
}
after · README.adoc

fleet logo

An NixOS cluster deployment tool.

Advantages over existing configuration systems (NixOps/Morph)

  • Modules can configure multiple hosts at once (I.e for wireguard/kubernetes installation)

  • Secrets can be securely stored in Git (No one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.

  • Automatic rollback on deployment failure, which will work, as long as system is passing initrd stage (So still be carefull with root filesystem mount)

Flake example

{
  description = "My cluster configuration";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
    fleet = {
      url = "github:CertainLach/fleet";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    flake-parts.url = "github:hercules-ci/flake-parts";
    lanzaboote = {
      url = "github:nix-community/lanzaboote/v0.3.0";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
  outputs = inputs:
    inputs.flake-parts.lib.mkFlake {inherit inputs;} {
      imports = [inputs.fleet.flakeModules.default];

      perSystem = {
        inputs',
        pkgs,
        system,
        ...
      }: {
        formatter = pkgs.alejandra;
        devShells.default =
          pkgs.mkShell {packages = [inputs'.fleet.packages.fleet];};
      };

      # Single flake may contain multiple fleet configurations, default one is called... `default`
      fleetConfigurations.default = {
        # nixos option section of fleet config declares module, which is used for all configured nixos hosts.
        nixos = {
          imports = [inputs.lanzaboote.nixosModules.lanzaboote];

          # Make `nix shell nixpkgs#thing` use the same nixpkgs, as used to build the system.
          nix.registry.nixpkgs = {
            from = {
              id = "nixpkgs";
              type = "indirect";
            };
            flake = inputs.nixpkgs;
            exact = false;
          };
        };

        # Those modules are used to configure all the machines in cluster at the same time, good example of global modules
        # Is I.e wiring up the mesh VPN, or deploying kubernetes, or other things.
        #
        # Modules use the same semantics as standard nixos module system, they are just configuring all the hosts at once.
        imports = [
          ./wireguard
          # Multi-instancible modules example
          (import ./kubernetes {hosts = ["a" "b"];})
          (import ./kubernetes {hosts = ["c" "d"];})
        ];

        # Hosts attribute (may also be defined/extended using modules attribute) configures hosts...
        hosts.controlplane-1 = {
          # Every host has some system, for which the system configuration needs to be built
          system = "x86_64-linux";
          nixos = {
            # And nixos modules
            imports = [
              ./controlplane-1/hardware-configuration.nix
              ./controlplane-1/configuration.nix
            ];
            # Configuration may also be specified inline, as in any nixos config.
            services.ray = {
              gpus = 4;
              cpus = 128;
            };
          };
        };
      };
    };
}

Secret generator example

TODO

This section should into some kind of fleet documentation…​ But as there is none, it is just left here as-is.

Quickly run securely setup gitlab

{config, ...}: {
  secrets = let ownership = { owner = "gitlab"; group = "gitlab"; }; in {
    gitlab-initial-root = {
      generator = {mkPassword}: mkPassword {};
    } // ownership;
    gitlab-secret = {
      generator = {mkPassword}: mkPassword {};
    } // ownership;
    gitlab-otp = {
      generator = {mkPassword}: mkPassword {};
    } // ownership;
    gitlab-db = {
      generator = {mkPassword}: mkPassword {};
    } // ownership;
    gitlab-jws = {
      generator = {mkRsa}: mkRsa {};
    } // ownership;
  };
  services.gitlab = let secrets = config.secrets; in {
    enable = true;
    initialRootPasswordFile = secrets.gitlab-initial-root.secretPath;
    secrets = {
      secretFile = secrets.gitlab-secret.secretPath;
      otpFile = secrets.gitlab-otp.secretPath;
      dbFile = secrets.gitlab-db.secretPath;
      jwsFile = secrets.gitlab-jws.secretPath;
    };
  };
}

Securely initialize kubernetes secrets

In my homelab and clusters, I almost always have some sort of HSM, and to issue new kubernetes certs I directly connect to it. This setup should probably split into multiple steps, where I allow target machine to generate CSR, then copy it to the HSM machine, and then sign it there…​ But this is just the plan. I want to build ansible-like script execution in fleet for this kind of tasks.

{...}: {
  # First I define required secret generators:
  nixpkgs.overlays = [
    (final: prev: let
      lib = final.lib;
    in {
      readKubernetesCa = {impureOn}:
        final.mkImpureSecretGenerator ''
          cd ~/ca

          cert=kubernetes-intermediateCA.crt

          expires_at=$(openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")
          echo -n $expires_at > $out/expires_at

          cat $cert > $out/public
        ''
        impureOn;
      mkKubernetesCert = {
        subj,
        sans ? [],
        impureOn,
      }:
        final.mkImpureSecretGenerator ''
          cd ~/ca

          params=$(sudo mktemp)
          csr=$(sudo mktemp)
          cert=$(sudo mktemp)
          sudo openssl ecparam -genkey -name secp384r1 -out $params
          sudo openssl req -new -key $params \
            -subj "${lib.strings.concatStringsSep "" (lib.attrsets.mapAttrsToList (k: v: "/${k}=${v}") subj)}" \
            ${lib.optionalString (sans != []) "-addext \"subjectAltName = ${lib.strings.concatStringsSep "," sans}\""} \
            -out $csr
          sudo hsms x509 -req -days 365 -in $csr -CA kubernetes-intermediateCA.crt -CAkey "pkcs11:object=[CENSORED] Kubernetes Intermediate CA;type=private" -CAcreateserial -copy_extensions copy -out $cert

          expires_at=$(sudo openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")
          echo -n $expires_at > $out/expires_at

          sudo cat $params | encrypt > $out/secret
          sudo cat $cert > $out/public
        ''
        impureOn;
    })
  ];
  # Those secret generators are impure, thus they are run in system environment.
  # Probably there needs to be a dedicated user for that kind of tasks, but this is my current setup, don't judge.
  # I write a couple of scripts for executing openssl with HSM.
  environment.systemPackages = [
    pkgs.openssl.bin
    (pkgs.writeShellApplication {
      name = "hsms";
      text = ''
        set -eu
        export OPENSSL_CONF=${openssl-conf}
        # Yay, using secrets to generate secrets!
        HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})
        exec ${pkgs.openssl}/bin/openssl "$@" -keyform=engine -CAkeyform=engine -engine=pkcs11 -passin=pass:"$HSM_PIN"
      '';
    })
    (pkgs.writeShellApplication {
      name = "hsmt";
      text = ''
        set -eu
        HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})
        exec ${pkgs.opensc}/bin/pkcs11-tool -l --pin="$HSM_PIN" "$@"
      '';
    })
  ];
  # And finally, I have secrets, which are shared between machines.
  # Note that this example is somewhat wrong, as this goes not into the machine configuration, but to fleet configuration.
  sharedSecrets = {
    "ca.pem" = {
      # This is just the public key, no need to regenerate it to change owner list
      regenerateOnOwnerAdded = false;
      # For secret regeneration/reencryption, we need to specify which machines SHOULD have it.
      expectedOwners = ["controlplane-1" "controlplane-2" "worker-1" "worker-2"];
      generator = {readKubernetesCa}:
        readKubernetesCa {
          impureOn = "[CENSORED]";
        };
    };
    "kube-admin.pem" = {
      regenerateOnOwnerAdded = false;
      expectedOwners = ["cluster-admin"];
      generator = {mkKubernetesCert}:
        mkKubernetesCert {
          subj = {
            CN = "admin";
            O = "system:masters";
          };
          impureOn = "[CENSORED]";
        };
    };
    "kube-apiserver.pem" = {
      # This secret depends on machine SANS, so if owner list has been changed, then we need to regenerate it.
      # However, SANS dependency is in fact handled by secret seed, and secret is regenerated if the seed is changed...
      #
      # In this case regeneration is added as a half-assed security measure, as if apiserver is removed, we don't
      # want for it to be able to pretend like it is a valid server.
      #
      # However, certificate revokation is complicated in my setup, and I can't show it here.
      regenerateOnOwnerAdded = true;
      expectedOwners = ["controlplane-1" "controlplane-2"];
      generator = {mkKubernetesCert}:
        mkKubernetesCert {
          inherit sans;
          subj.CN = "kubernetes";
          impureOn = "[CENSORED]";
        };
    };
}
addeddocs/logo.svgdiffbeforeafterboth
--- /dev/null
+++ b/docs/logo.svg
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   version="1.1"
+   id="svg1"
+   width="960"
+   height="400"
+   viewBox="0 0 960 400"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1">
+    <color-profile
+       name="sRGB IEC61966-2.1"
+       xlink:href="data:application/vnd.iccprofile;base64,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"
+       id="color-profile1" />
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath25">
+      <path
+         d="M 0,300 H 720 V 0 H 0 Z"
+         transform="translate(-669.99999)"
+         id="path25" />
+    </clipPath>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath27">
+      <path
+         d="M 0,300 H 720 V 0 H 0 Z"
+         transform="translate(-268.88378,-104.48931)"
+         id="path27" />
+    </clipPath>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath29">
+      <path
+         d="M 0,300 H 720 V 0 H 0 Z"
+         transform="translate(-160.13303,-120.13881)"
+         id="path29" />
+    </clipPath>
+  </defs>
+  <g
+     id="layer-MC0"
+     style="display:none"
+     transform="translate(-5680)">
+    <path
+       id="path23"
+       d="M 0,0 H 720 V 300 H 0 Z"
+       style="fill:#E6E6F4;fill-opacity:1;fill-rule:nonzero;stroke:none"
+       transform="matrix(1.3333333,0,0,-1.3333333,5680,400)" />
+  </g>
+  <g
+     id="layer-MC1"
+     transform="translate(-5680)">
+    <path
+       id="path24"
+       d="m 0,0 h -620 c -27.614,0 -50,22.386 -50,50 v 200 c 0,27.614 22.386,50 50,50 H 0 c 27.614,0 50,-22.386 50,-50 V 50 C 50,22.386 27.614,0 0,0"
+       style="fill:#111216;fill-opacity:1;fill-rule:nonzero;stroke:none"
+       transform="matrix(1.3333333,0,0,-1.3333333,6573.3333,400)"
+       clip-path="url(#clipPath25)" />
+    <path
+       id="path26"
+       d="m 0,0 c -0.646,5.849 -1.577,11.611 -2.771,17.277 -0.276,1.308 -1.582,2.131 -2.886,1.837 L -20.834,15.689 -67.954,5.054 c -1.501,-0.339 -2.928,0.802 -2.928,2.341 v 14.922 c 0,1.121 0.776,2.093 1.869,2.341 l 42.777,9.697 17.667,4.005 c -2.07,6.041 -4.442,11.942 -7.118,17.673 -2.652,5.683 -5.604,11.196 -8.814,16.537 -2.968,4.937 -6.163,9.72 -9.575,14.334 -3.881,5.246 -8.043,10.27 -12.459,15.058 -7.426,8.053 -15.573,15.427 -24.347,22.017 -0.312,0.234 -0.627,0.464 -0.941,0.696 -5.462,4.048 -11.146,7.812 -17.058,11.231 -0.024,-0.013 -0.03,-0.017 -0.054,-0.031 v -21.131 -0.07 -21.235 c 0,-1.585 1.509,-2.735 3.037,-2.314 l 15.016,4.134 3.21,0.883 c 0.826,0.228 1.716,0.002 2.327,-0.598 3.339,-3.282 6.537,-6.706 9.583,-10.265 1.17,-1.366 0.493,-3.486 -1.255,-3.908 l -13.865,-3.343 -16.216,-3.911 c -1.078,-0.26 -1.837,-1.224 -1.837,-2.333 V 60.372 c 0,-1.548 1.445,-2.691 2.952,-2.335 l 15.101,3.569 26.234,6.202 c 0.996,0.235 2.039,-0.181 2.588,-1.044 2.494,-3.925 4.822,-7.964 6.977,-12.108 0.721,-1.387 -0.083,-3.085 -1.607,-3.434 l -34.192,-7.833 -16.189,-3.708 c -1.091,-0.25 -1.864,-1.221 -1.864,-2.34 V 20.142 2.237 c 0,-1.122 -0.777,-2.094 -1.872,-2.341 l -16.074,-3.628 -44.93,-10.14 -9.018,-2.036 -18.051,-4.074 c -0.002,-0.013 -0.002,-0.014 -0.004,-0.026 5.884,-3.404 11.99,-6.464 18.271,-9.197 16.744,-7.285 34.801,-12.116 53.732,-13.996 5.92,-0.588 11.924,-0.893 17.999,-0.893 6.075,0 12.079,0.305 18,0.893 18.931,1.88 36.988,6.711 53.732,13.996 6.279,2.732 12.383,5.791 18.266,9.194 0,0 0,10e-4 0,10e-4 C 1.117,-13.245 0.726,-6.572 0,0"
+       style="fill:#E6E6F4;fill-opacity:1;fill-rule:nonzero;stroke:none"
+       transform="matrix(1.3333333,0,0,-1.3333333,6038.5117,260.68093)"
+       clip-path="url(#clipPath27)" />
+    <path
+       id="path28"
+       d="m 0,0 c 1.093,0.248 1.869,1.22 1.869,2.341 v 17.152 17.954 16.25 16.001 15.388 23.244 C -16.706,94.376 -32.491,76.918 -44.506,56.931 -47.718,51.588 -50.671,46.072 -53.325,40.387 -56,34.656 -58.372,28.757 -60.441,22.717 -62.53,16.623 -64.29,10.379 -65.723,4.009 -67.168,-2.42 -68.275,-8.976 -69.012,-15.645 l 18.687,4.236 z"
+       style="fill:#E6E6F4;fill-opacity:1;fill-rule:nonzero;stroke:none"
+       transform="matrix(1.3333333,0,0,-1.3333333,5893.5107,239.81493)"
+       clip-path="url(#clipPath29)" />
+    <text
+       id="text29"
+       xml:space="preserve"
+       transform="matrix(1.3333333,0,0,1.3333333,6133.9095,248.32227)"><tspan
+         id="tspan29"
+         style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:120px;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans Semi-Bold';writing-mode:lr-tb;fill:#E6E6F4;fill-opacity:1;fill-rule:nonzero;stroke:none"
+         x="0"
+         y="0">fleet</tspan></text>
+  </g>
+</svg>
deleteddocs/tmplogo.pngdiffbeforeafterboth

binary blob — no preview