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

Add push-cache-effect #165

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
160 changes: 160 additions & 0 deletions effects/push-cache/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
{ lib, withSystem, config, ... }:
let pkgs-x86_64-linux = withSystem "x86_64-linux" ({ pkgs, ... }: pkgs);
Copy link

@brainrake brainrake Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should support other systems.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would see what happens if we try to push non x86_64-linux derivations using an x86_64-linux effect before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those will be scheduled during the build phase of the job, and substituted into the agent's store before the effect is launched.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested: pushing derivations built on other systems to attic works.

in {
imports = [ ../../flake-module.nix ];

options = {
push-cache-effect = {
Copy link

@brainrake brainrake Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have a cachix-push-effect and attic-push-effect to mirror cachix-deploy-effect instead of the type option. That could resolve passing the type in an unobtrusive way.

enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enables an effect that pushes certain outputs to a different binary cache.

Hercules CI normally pushes everything to the cache(s) configured on the agent. This effect supplements that behavior by letting you push a subset of those to a different cache.
Note that it only pushes the output closure, and not the closures of build dependencies used during the build stage of the CI job. (Unless those closures happen to also be part of the output or "runtime" closure)
'';
};
attic-client-pkg = lib.mkOption {
type = lib.types.package;
description = ''
Version of the attic-client package to use on \"x86_64-linux\".

Hint: You can use `attic.packages.x86_64-linux.attic-client` from the attic flake.
'';
default = pkgs-x86_64-linux.attic-client or (throw
"push-cache-effect.attic-client-pkg: It seems that attic hasn't been packaged in Nixpkgs (yet?). Please check <nixpkgs packaging request issue> or set <option> manually.");
};
cachix-pkg = lib.mkOption {
type = lib.types.package;
default = pkgs-x86_64-linux.cachix;
description =
''Version of the cachix package to use on "x86_64-linux".'';
};
caches = lib.mkOption {
description = ''
An attribute set, each `name: value` pair translates to
an effect under `onPush.default.outputs.effects.push-cache-effect.name`.
'';
example = ''
{
our-cachix = {
type = \"cachix\";
secretName = \"our-cachix-token\";
branches = [ \"master\" ];
packages = [ pkgs.hello ];
};
}
'';
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
options = {
name = lib.mkOption {
Copy link

@brainrake brainrake Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default should be be cachix|attic-push-effect or cachix|attic-push-${attrname}-effect to mirror cachix-deploy-effect.

type = lib.types.str;
default = name;
description = ''
Name of the effect. By default it's the attribute name.
'';
};
type = lib.mkOption {
type = lib.types.enum [ "attic" "cachix" ];
description = ''A string "attic" or "cachix".'';
};
packages = lib.mkOption {
type = with lib.types; listOf package;
description = "List of packages to push to the cache.";
example = "[ pkgs.hello ]";
Copy link

@brainrake brainrake Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a default, we could use flakeToOutputs from hercules-ci-agent to match the behavior of the default build
https://github.com/hercules-ci/hercules-ci-agent/blob/0e9e67bb7d41ea2bbb13982887ff3b9c511a2aba/hercules-ci-agent/data/default-herculesCI-for-flake.nix#L48
or combine herculesCI.onPush.*.outputs to match the hercules ci build phase
https://docs.hercules-ci.com/hercules-ci-agent/evaluation#attribute-herculesCI.onPush-outputs

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! Didn't know flakeToOutputs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would involve writing a attribute tree traversal to match the agent behavior, with the right recuresIntoAttrs behavior, rejecting things with _type, etc.
That could be done, but I wouldn't encode into this module the assumption that every output should be pushed.

Note also that this effect can not replicate the whole agent behavior, such as pushing all the right build dependencies.
This means that it's not a replacement for the cache that's used by the agents transparently.
More significantly, it also means that this effect is not 100% suitable as a build cache or "development cache", so I doubt that e.g. the checks or the devShell dependencies should be pushed by it. A better use case for this effect is as a "release cache" - for only the end products, such as packages, or perhaps also paths for deployment, such as nixosConfigurations or the inputs of effects (if flake consumers use the same effect functions).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved: we leave it unspecified and the responsibility of the caller since replicating default builder behavior is infeasible.

};
secretName = lib.mkOption {
type = lib.types.str;
description = ''
Name of the HerculesCI secret. See [HerculesCI docs](https://docs.hercules-ci.com/hercules-ci-agent/secrets-json).
The secrets "data" field should contain given data:

```
"data": {
"name": "my-cache-name",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should name and endpoint be in the secret? I think the attribute name can be used as name, and the endpoint doesn't need to be secret, it can be an option. But it's convenient to manage together with the token.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both are acceptable solutions.

"token": "ey536428341723812",
"endpoint": "https://my-cache-name.com"
}
```

The "endpoint" field is needed for Attic cache. With Cachix cache the "endpoint" field is not read and can be absent.
'';
};
branches = lib.mkOption {
type = with lib.types; nullOr (listOf str);
default = null;
description = ''
Branches on which we'd like to execute the effect. Set to `null` to execute on all branches.
'';
};
};
}));
};
};
};

config = let
# file with all the package paths written line by line
# nixpkgs -> [derivation] -> derivation
packagesFile = pkgs: packages:
pkgs.writeText "pushed-paths" (lib.strings.concatStringsSep "\n"
(builtins.map builtins.toString packages));

mkAtticPushEffect = { cacheOptions, branch, }:
withSystem "x86_64-linux" ({ hci-effects, pkgs, ... }:
let
pushEffect = hci-effects.mkEffect {
inputs = [ config.push-cache-effect.attic-client-pkg ];
secretsMap = { token-file = "${cacheOptions.secretName}"; };
userSetupScript = ''
attic login \
default \
$(readSecretString token-file .endpoint) \
$(readSecretString token-file .token)
'';
effectScript = ''
cat ${
packagesFile pkgs cacheOptions.packages
} | xargs -s 4096 attic push default:$(readSecretString token-file .name)
'';
};
in hci-effects.runIf ((cacheOptions.branches == null)
|| (builtins.elem branch cacheOptions.branches)) pushEffect);

mkCachixPushEffect = { cacheOptions, branch, }:
withSystem "x86_64-linux" ({ hci-effects, pkgs, ... }:
let
pushEffect = hci-effects.mkEffect {
inputs = [ config.push-cache-effect.cachix-pkg ];
secretsMap = { token-file = "${cacheOptions.secretName}"; };
userSetupScript = ''
cachix authtoken $(readSecretString token-file .token)
'';
effectScript = ''
cat ${
packagesFile pkgs cacheOptions.packages
} | cachix push $(readSecretString token-file .name)
'';
};
in hci-effects.runIf ((cacheOptions.branches == null)
|| (builtins.elem branch cacheOptions.branches)) pushEffect);
in lib.mkIf config.push-cache-effect.enable {
herculesCI = herculesConfig: {
onPush.default.outputs.effects.push-cache-effect = lib.attrsets.mapAttrs'
(_: cacheOptions: {
inherit (cacheOptions) name;
value = builtins.getAttr "${cacheOptions.type}" {
attic = mkAtticPushEffect {
inherit cacheOptions;
inherit (herculesConfig.config.repo) branch;
};
cachix = mkCachixPushEffect {
inherit cacheOptions;
inherit (herculesConfig.config.repo) branch;
};
};
}) config.push-cache-effect.caches;
};
};
}
2 changes: 2 additions & 0 deletions flake-public-outputs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

flakeModule = ./flake-module.nix;

push-cache-effect = ./effects/push-cache;

lib.withPkgs = pkgs:
let effects = import ./effects/default.nix effects pkgs;
in effects;
Expand Down