Compare commits

..

11 Commits

Author SHA1 Message Date
Linus Gasser cd25c346c1 fix(app): keep recipe version on a single env field and label
Store the deployed version on TYPE (or the legacy RECIPE key when TYPE is
absent) instead of duplicating it across both. For git-URL recipes RECIPE
now stays a bare source pointer, so the two can never drift apart. Drops
the dead dirtyVersion branch and the fragile substring skip in
WriteRecipeVersion.

Also stamp the coop-cloud.<stack>.version label ourselves on upgrade and
rollback rather than trusting the value baked into the recipe's
compose.yml: git-URL/WIP recipes carry a stale or missing label, which
otherwise left the live deployment reporting the wrong version. Stop
getReleaseNotes and the [version] arg validation from fataling when the
deployed version is "unknown".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:31:58 +02:00
Linus Gasser 0397443de3 feat(app): support git-URL recipes in 'abra app new'
Allow `abra app new <git-url>` to use a recipe from outside the
catalogue. On clone, a `.abra-source` sidecar records the canonical
host/path name (the on-disk directory escapes "/" and "." lossily), and
IsClean ignores it. When templating the app's .env, a `RECIPE=<canonical
name>` line is injected so a later `abra app deploy`, possibly on another
machine, re-fetches the recipe from the same git source. `recipe ls` now
shows a source column listing these external recipes alongside catalogue
ones.
2026-07-21 08:13:37 +02:00
Linus Gasser 22cd3e1307 feat(recipe): normalize git URLs to canonical host/path names
Add NormalizeRecipeName to unify the various ways a recipe can be
referenced - https/http/ssh URLs, SCP-style git@host:path, already-
canonical host/path, and short catalogue names - to a single stable
"host/path" form, preserving any ":version" suffix. Wire it into
recipe.Get and ValidateRecipe so every entry point accepts git URLs, and
add Recipe.ShortName to recover the bare recipe name from a prefixed one.
2026-07-21 08:13:11 +02:00
Linus Gasser 0ed1881374 fix(recipe): reject branch names as :version with a clear error
Passing a branch as the ":version" suffix previously failed with an
opaque "unable to resolve" error from go-git. Detect when the requested
revision matches a branch on a configured remote and fail with a message
explaining that ":<version>" only supports tags or commit hashes.
2026-07-21 08:12:57 +02:00
Linus Gasser 31fe2fe2a6 fix(formatter): guard ShortenID/SmallSHA against short input
ShortenID and SmallSHA sliced their input to a fixed length without
checking it was long enough, panicking on shorter strings. Return the
input unchanged when it is already shorter than the cut. Also replace the
blank Commit placeholder with an explicit "unknown-commit" sentinel.
2026-07-21 08:12:32 +02:00
renovate-bot 0dec68294f chore(deps): update nginx docker tag to v1.31.3
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-16 02:01:04 +00:00
renovate-bot 69ba548dab chore(deps): update dependency codespell to v2.4.3
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-15 12:00:52 +00:00
renovate-bot cfa9ff15bb chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-08 18:01:07 +00:00
renovate-bot ddf7441152 chore(deps): update module golang.org/x/term to v0.45.0
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-08 17:01:50 +00:00
renovate-bot 20b4a3fb84 chore(deps): update golang docker tag
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-08 14:01:06 +00:00
renovate-bot 81f4420a67 chore(deps): update module golang.org/x/sys to v0.47.0
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2026-07-08 13:01:44 +00:00
64 changed files with 753 additions and 839 deletions
-35
View File
@@ -84,38 +84,3 @@ build-mo:
release:
@goreleaser release --clean
CLIENT_VM := abra-client
vm-client-status:
sudo systemctl status microvm@$(CLIENT_VM).service
vm-client-create:
sudo microvm -f git+file://$$(pwd) -c $(CLIENT_VM)
vm-client-start:
sudo systemctl start microvm@$(CLIENT_VM).service
vm-client-update:
sudo microvm -R -f git+file://$$(pwd) -u $(CLIENT_VM)
vm-client-run:
sudo microvm -f git+file://$$(pwd) -r $(CLIENT_VM)
vm-client-stop:
sudo systemctl stop microvm@$(CLIENT_VM)
vm-client-delete: vm-client-stop
sudo rm -rf /var/lib/microvms/$(CLIENT_VM)
SERVER_VM := abra-server
vm-server-status:
sudo systemctl status microvm@$(SERVER_VM).service
vm-server-create:
sudo microvm -f git+file://$$(pwd) -c $(SERVER_VM)
vm-server-start:
sudo systemctl start microvm@$(SERVER_VM).service
vm-server-update:
sudo microvm -R -f git+file://$$(pwd) -u $(SERVER_VM)
vm-server-run:
sudo microvm -f git+file://$$(pwd) -r $(SERVER_VM)
vm-server-stop:
sudo systemctl stop microvm@$(SERVER_VM)
vm-server-delete: vm-stop
sudo rm -rf /var/lib/microvms/$(SERVER_VM)
test-integration:
ssh-keygen -t ed25519 -f ./test-integration -q -N "" -C "abra integration test"
+7 -1
View File
@@ -222,7 +222,13 @@ synchronize your local app environment values with what is deployed live.`),
mergedEnv[k] = v
}
if !strings.Contains(recipeEnvVar, ":") {
// The recipe version is always carried by TYPE (RECIPE, when present,
// is a bare source pointer). Stamp the deployed version onto TYPE and
// leave RECIPE untouched so the two never drift apart.
if t, ok := mergedEnv["TYPE"]; ok {
name, _, _ := strings.Cut(t, ":")
mergedEnv["TYPE"] = fmt.Sprintf("%s:%s", name, version)
} else {
mergedEnv[recipeKey] = fmt.Sprintf("%s:%s", mergedEnv[recipeKey], version)
}
+17 -1
View File
@@ -32,6 +32,18 @@ deploy <domain>" to do so.
You can see what recipes are available (i.e. values for the [recipe] argument)
by running "abra recipe ls".
In addition to short catalogue names, [recipe] also accepts arbitrary git
URLs to use a recipe from outside the catalogue (e.g. a fork or work in
progress). Any of these forms is accepted:
abra app new git.example.com/user/recipe
abra app new https://git.example.com/user/recipe
abra app new git@git.example.com:user/recipe
In that case a RECIPE=<canonical name> line is written to the app's .env
file so a subsequent "abra app deploy" (on this or another machine) will
re-fetch the recipe from the same git source.
Recipe commit hashes are supported values for "[version]".
Passing the "--secrets/-S" flag will automatically generate secrets for your
@@ -295,7 +307,7 @@ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
if appDomain == "" && !internal.NoInput {
prompt := &survey.Input{
Message: i18n.G("Specify app domain"),
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
Default: fmt.Sprintf("%s.%s", recipe.ShortName(), server),
}
if err := survey.AskOne(prompt, &appDomain); err != nil {
return err
@@ -306,6 +318,10 @@ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
return errors.New(i18n.G("no domain provided"))
}
if strings.ContainsAny(appDomain, "/\\") {
return errors.New(i18n.G("invalid domain '%s': must not contain '/' or '\\'", appDomain))
}
return nil
}
+6
View File
@@ -188,6 +188,12 @@ beforehand. See "abra app backup" for more.`),
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
}
// NOTE: stamp the deployed version label ourselves rather than relying
// on the value baked into the recipe's compose.yml. Git-URL/WIP recipes
// often carry a stale or missing label, which would otherwise leave the
// live deployment reporting the wrong version after a rollback.
appPkg.SetVersionLabel(compose, stackName, chosenDowngrade)
// Gather secrets
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
if err != nil {
+24 -4
View File
@@ -200,6 +200,12 @@ beforehand. See "abra app backup" for more.`),
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
}
// NOTE: stamp the deployed version label ourselves rather than relying
// on the value baked into the recipe's compose.yml. Git-URL/WIP recipes
// often carry a stale or missing label, which would otherwise leave the
// live deployment reporting the wrong version after an upgrade.
appPkg.SetVersionLabel(compose, stackName, chosenUpgrade)
envVars, err := appPkg.CheckEnv(app)
if err != nil {
log.Fatal(err)
@@ -346,9 +352,17 @@ func getReleaseNotes(
return errors.New(i18n.G("parsing chosen upgrade version failed: %s", err))
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return errors.New(i18n.G("parsing deployment version failed: %s", err))
// The deployed version can be "unknown" (e.g. deployed without a version
// label, as is common for git-URL/work-in-progress recipes). In that case
// there is no lower bound to filter by, so we gather notes for every
// version below the chosen one instead of failing.
var parsedDeployedVersion tagcmp.Tag
haveDeployedVersion := deployMeta.Version != config.UNKNOWN_DEFAULT
if haveDeployedVersion {
parsedDeployedVersion, err = tagcmp.Parse(deployMeta.Version)
if err != nil {
return errors.New(i18n.G("parsing deployment version failed: %s", err))
}
}
for _, version := range internal.SortVersionsDesc(versions) {
@@ -357,7 +371,7 @@ func getReleaseNotes(
return errors.New(i18n.G("parsing recipe version failed: %s", err))
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
if (!haveDeployedVersion || parsedVersion.IsGreaterThan(parsedDeployedVersion)) &&
parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version, app.Domain)
if err != nil {
@@ -420,6 +434,12 @@ func validateUpgradeVersionArg(
return errors.New(i18n.G("'%s' is not a known version for %s", specificVersion, app.Recipe.Name))
}
// Without a known deployed version we cannot tell whether the requested
// version is an upgrade; trust the explicit choice rather than failing.
if deployMeta.Version == config.UNKNOWN_DEFAULT {
return nil
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return errors.New(i18n.G("'%s' is not a known version", deployMeta.Version))
+9 -3
View File
@@ -59,9 +59,15 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
log.Fatal(i18n.G("no recipe name provided"))
}
if _, ok := knownRecipes[recipeName]; !ok {
if !strings.Contains(recipeName, "/") {
log.Fatal(i18n.G("no recipe '%s' exists?", recipeName))
recipeName = recipe.NormalizeRecipeName(recipeName)
lookupName := recipeName
if i := strings.LastIndex(lookupName, ":"); i >= 0 {
lookupName = lookupName[:i]
}
if _, ok := knownRecipes[lookupName]; !ok {
if !strings.Contains(lookupName, "/") {
log.Fatal(i18n.G("no recipe '%s' exists? pass a git URL (e.g. https://git.example.com/user/recipe) to use a recipe outside the catalogue", lookupName))
}
}
+45
View File
@@ -2,11 +2,13 @@ package recipe
import (
"fmt"
"path"
"sort"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
@@ -41,6 +43,7 @@ var RecipeListCommand = &cobra.Command{
headers := []string{
i18n.G("name"),
i18n.G("source"),
i18n.G("category"),
i18n.G("status"),
i18n.G("healthcheck"),
@@ -56,6 +59,7 @@ var RecipeListCommand = &cobra.Command{
for _, recipe := range recipes {
row := []string{
recipe.Name,
i18n.G("catalogue"),
recipe.Category,
strconv.Itoa(recipe.Features.Status),
recipe.Features.Healthcheck,
@@ -76,6 +80,23 @@ var RecipeListCommand = &cobra.Command{
}
}
externals := externalRecipes(catl)
sort.Strings(externals)
for _, name := range externals {
row := []string{
name,
i18n.G("external"),
"-", "-", "-", "-", "-", "-", "-",
}
if pattern != "" {
if !strings.Contains(name, pattern) {
continue
}
}
table.Row(row...)
rows = append(rows, row)
}
if len(rows) > 0 {
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
@@ -93,6 +114,30 @@ var RecipeListCommand = &cobra.Command{
},
}
// externalRecipes returns canonical names of locally-cloned recipes that
// were sourced from an arbitrary git URL (i.e. they carry a .abra-source
// sidecar) and are not already listed in the catalogue.
func externalRecipes(catl recipe.RecipeCatalogue) []string {
dirs, err := recipe.GetRecipesLocal()
if err != nil {
log.Debug(i18n.G("can't read local recipes: %s", err))
return nil
}
var names []string
for _, dir := range dirs {
canonical := recipe.ReadRecipeSource(path.Join(config.RECIPES_DIR, dir))
if canonical == "" {
continue
}
if _, inCatalogue := catl[canonical]; inCatalogue {
continue
}
names = append(names, canonical)
}
return names
}
var (
pattern string
)
+1 -1
View File
@@ -16,7 +16,7 @@ func main() {
Version = "dev"
}
if Commit == "" {
Commit = " "
Commit = "unknown-commit"
}
cli.Run(Version, Commit)
Generated
-60
View File
@@ -18,48 +18,6 @@
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1785119570,
"narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-26.05",
"repo": "home-manager",
"type": "github"
}
},
"microvm": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"spectrum": "spectrum"
},
"locked": {
"lastModified": 1784666190,
"narHash": "sha256-xgfS6slV7J3baMooNN1UuBi51RIgg9y0DbCxfSA0668=",
"owner": "astro",
"repo": "microvm.nix",
"rev": "fa5340ac684cdce8a22b6d4a0bcebb0cc999275e",
"type": "github"
},
"original": {
"owner": "astro",
"repo": "microvm.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1778443072,
@@ -79,27 +37,9 @@
"root": {
"inputs": {
"flake-utils": "flake-utils",
"home-manager": "home-manager",
"microvm": "microvm",
"nixpkgs": "nixpkgs"
}
},
"spectrum": {
"flake": false,
"locked": {
"lastModified": 1783694892,
"narHash": "sha256-xO8f7Qng+18FK2UlB9vcrkxCaQMCt5WjCH24aW/11eg=",
"ref": "refs/heads/main",
"rev": "24c4346e30fdea8d8e80f34aec3554a15a667d24",
"revCount": 1410,
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
},
"original": {
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
+20 -56
View File
@@ -3,71 +3,35 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager/release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
microvm = {
url = "github:astro/microvm.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
microvm,
home-manager,
flake-utils,
}:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
publicKey = builtins.readFile ./test-integration.pub;
privateKey = builtins.readFile ./test-integration;
in
{
nixosConfigurations = {
abra-client = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
home-manager.nixosModules.home-manager
./nix/hosts/client/configuration.nix
];
specialArgs = { inherit publicKey privateKey; };
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages = rec {
abra = pkgs.callPackage ./package.nix { };
default = abra;
};
abra-server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
./nix/hosts/server/configuration.nix
];
specialArgs = { inherit publicKey privateKey; };
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
};
};
packages = rec {
abra = pkgs.callPackage ./package.nix { };
default = abra;
};
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
};
devShells.${system}.default = pkgs.mkShell {
# testing env variables
BATS_LIB_PATH = "~/.local/share/bats/";
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
packages = with pkgs; [
go_1_26
gnumake
gopls
];
};
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go_1_26
gnumake
];
};
}
);
}
+2 -2
View File
@@ -20,7 +20,7 @@ require (
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.19.1
golang.org/x/term v0.44.0
golang.org/x/term v0.45.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
)
@@ -155,7 +155,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.46.0
golang.org/x/sys v0.47.0
)
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
+4 -4
View File
@@ -1139,13 +1139,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-162
View File
@@ -1,162 +0,0 @@
{
lib,
pkgs,
publicKey,
privateKey,
...
}:
let
index = 2;
mac = "00:00:00:00:00:01";
serverIp = "10.0.0.3";
in
{
microvm = {
vcpu = 4;
mem = 2049; # see issue related in microvm repo with 2048, so add 1MB
interfaces = [
{
id = "vm${toString index}";
type = "tap";
inherit mac;
}
];
shares = [
{
proto = "virtiofs";
tag = "repo";
# Source path can be absolute or relative
# to /var/lib/microvms/$hostName
source = "/home/dev/Documents/abra";
mountPoint = "/home/dev/abra";
}
];
};
boot.tmp = {
useTmpfs = true;
tmpfsSize = "2G";
};
networking = {
hostName = "abra-client";
useNetworkd = true;
};
systemd.network.networks."10-eth" = {
matchConfig.MACAddress = mac;
# Static IP configuration
address = [
"10.0.0.${toString index}/32"
"fec0::${lib.toHexString index}/128"
];
routes = [
{
# A route to the host
Destination = "10.0.0.0/32";
GatewayOnLink = true;
}
{
# Route to server
Destination = "${serverIp}/32";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "0.0.0.0/0";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "::/0";
Gateway = "fec0::";
GatewayOnLink = true;
}
];
networkConfig = {
# DNS servers no longer come from DHCP nor Router
# Advertisements. Perhaps you want to change the defaults:
DNS = [
# Quad9.net
"9.9.9.9"
"149.112.112.112"
"2620:fe::fe"
"2620:fe::9"
];
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [ 22 ];
networking.firewall.allowedUDPPorts = [ 22 ];
networking.extraHosts = ''
${serverIp} abra.local
${serverIp} app_check_bats.abra.local
'';
environment.variables = {
CGO_ENABLED = 0;
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
};
users.users.dev = {
isNormalUser = true;
password = "test";
home = "/home/dev";
description = "Alice Foobar";
extraGroups = [
"wheel"
"networkmanager"
];
openssh.authorizedKeys.keys = [
publicKey
];
};
# ssh agent with askpass
programs.ssh = {
startAgent = true;
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
AllowUsers = [ "dev" ];
};
};
programs.git.enable = true;
environment.systemPackages = with pkgs; [
# build dependencies, copied from flake.nix
go_1_26
gnumake
# testing dependencies
(bats.withLibraries (p: [
p.bats-assert
p.bats-file
p.bats-support
]))
jq
];
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.dev =
{ ... }:
{
imports = [
./home.nix
];
};
# pass args to home-manager
extraSpecialArgs = {
publicKey = publicKey;
privateKey = privateKey;
serverIp = serverIp;
};
};
}
-41
View File
@@ -1,41 +0,0 @@
{
serverIp,
...
}:
{
# Home Manager needs a bit of information about you and the
# paths it should manage.
home.username = "dev";
home.homeDirectory = "/home/dev";
# This value determines the Home Manager release that your
# configuration is compatible with. This helps avoid breakage
# when a new Home Manager release introduces backwards
# incompatible changes.
#
# You can update Home Manager without changing this value. See
# the Home Manager release notes for a list of state version
# changes in each release.
home.stateVersion = "26.05";
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
"abra.local" = {
HostName = serverIp;
User = "dev";
Port = 22;
IdentityFile = "/home/dev/abra/test-integration";
};
"*.abra.local" = {
HostName = serverIp;
User = "dev";
Port = 22;
IdentityFile = "/home/dev/abra/test-integration";
};
};
};
}
-116
View File
@@ -1,116 +0,0 @@
{
lib,
publicKey,
...
}:
let
index = 3;
mac = "00:00:00:00:00:02";
clientIp = "10.0.0.2";
in
{
microvm = {
vcpu = 2;
mem = 2049; # see issue related in microvm repo with 2048, so add 1MB
interfaces = [
{
id = "vm${toString index}";
type = "tap";
inherit mac;
}
];
};
boot.tmp = {
useTmpfs = true;
tmpfsSize = "2G";
};
networking = {
hostName = "abra-server";
useNetworkd = true;
};
systemd.network.networks."11-eth" = {
matchConfig.MACAddress = mac;
# Static IP configuration
address = [
"10.0.0.${toString index}/32"
"fec0::${lib.toHexString index}/128"
];
routes = [
{
# A route to the host
Destination = "10.0.0.0/32";
GatewayOnLink = true;
}
{
# Route to server
Destination = "${clientIp}/32";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "0.0.0.0/0";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "::/0";
Gateway = "fec0::";
GatewayOnLink = true;
}
];
networkConfig = {
# DNS servers no longer come from DHCP nor Router
# Advertisements. Perhaps you want to change the defaults:
DNS = [
# Quad9.net
"9.9.9.9"
"149.112.112.112"
"2620:fe::fe"
"2620:fe::9"
];
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [
22
80
443
];
networking.firewall.allowedUDPPorts = [
22
80
443
];
users.users.dev = {
isNormalUser = true;
password = "test";
home = "/home/dev";
description = "Alice Foobar";
extraGroups = [
"wheel"
"networkmanager"
"docker"
];
openssh.authorizedKeys.keys = [
publicKey
];
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
AllowUsers = [ "dev" ];
};
};
virtualisation.docker = {
enable = true;
liveRestore = false;
};
}
+111 -51
View File
@@ -260,12 +260,30 @@ func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"]
recipeName, exists := env["RECIPE"]
if !exists {
recipeName, exists = env["TYPE"]
if !exists {
return App{}, errors.New(i18n.G("%s is missing the TYPE env var?", name))
}
typeVar, hasType := env["TYPE"]
recipeVar, hasRecipe := env["RECIPE"]
if !hasType && !hasRecipe {
return App{}, errors.New(i18n.G("%s is missing the TYPE env var?", name))
}
// The recipe identity is taken from RECIPE when present (it records the
// canonical git source for externally-sourced recipes), otherwise from
// TYPE. The deployed version is always carried by TYPE when a TYPE line
// exists; only legacy apps that set RECIPE alone carry it there. RECIPE
// itself must stay a bare source pointer, never a versioned value.
identity := typeVar
if hasRecipe {
identity = recipeVar
}
versionSource := identity
if hasType {
versionSource = typeVar
}
recipeName, _, _ := strings.Cut(identity, ":")
if _, version, found := strings.Cut(versionSource, ":"); found && version != "" {
recipeName = fmt.Sprintf("%s:%s", recipeName, version)
}
return App{
@@ -392,11 +410,15 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
newContents := strings.Replace(
string(read),
fmt.Sprintf("%s.example.com", r.Name),
fmt.Sprintf("%s.example.com", r.ShortName()),
domain,
-1,
)
if strings.Contains(r.Name, "/") {
newContents = injectRecipeLine(newContents, r.Name)
}
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
return err
@@ -407,6 +429,37 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
return nil
}
// injectRecipeLine ensures the env file carries a RECIPE=<canonical name>
// line so a downstream `abra app deploy` (potentially on another machine)
// can re-fetch the recipe from its original git source. If a RECIPE= line
// already exists in the copied .env.sample it is replaced; otherwise a new
// line is inserted immediately after the TYPE= line, or appended at the
// end if no TYPE= line is present.
func injectRecipeLine(contents, canonicalName string) string {
lines := strings.Split(contents, "\n")
for i, line := range lines {
trimmed := strings.TrimLeft(line, " \t")
if strings.HasPrefix(trimmed, "RECIPE=") {
lines[i] = "RECIPE=" + canonicalName
return strings.Join(lines, "\n")
}
}
for i, line := range lines {
trimmed := strings.TrimLeft(line, " \t")
if strings.HasPrefix(trimmed, "TYPE=") {
out := make([]string, 0, len(lines)+1)
out = append(out, lines[:i+1]...)
out = append(out, "RECIPE="+canonicalName)
out = append(out, lines[i+1:]...)
return strings.Join(out, "\n")
}
}
if contents != "" && !strings.HasSuffix(contents, "\n") {
contents += "\n"
}
return contents + "RECIPE=" + canonicalName + "\n"
}
// SanitiseAppName makes a app name usable with Docker by replacing illegal
// characters.
func SanitiseAppName(name string) string {
@@ -514,10 +567,22 @@ func ExposeAllEnv(
_, exists := service.Environment[k]
if !exists {
value := v
if k == "TYPE" || k == "RECIPE" {
switch k {
case "TYPE":
// NOTE(d1): don't use the wrong version from the app env
// since we are deploying a new version
value = toDeployVersion
// since we are deploying a new version. Keep the
// recipe name already recorded in TYPE and only
// swap in the version being deployed.
name, _, _ := strings.Cut(v, ":")
if _, version, found := strings.Cut(toDeployVersion, ":"); found {
value = fmt.Sprintf("%s:%s", name, version)
} else {
value = name
}
case "RECIPE":
// RECIPE records the canonical git source only; the
// version is carried by TYPE, so keep it bare here.
value, _, _ = strings.Cut(toDeployVersion, ":")
}
service.Environment[k] = &value
log.Debug(i18n.G("%s: %s: %s", stackName, k, value))
@@ -642,60 +707,55 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
if err != nil {
return err
}
defer file.Close()
var (
dirtyVersion string
skipped bool
lines []string
scanner = bufio.NewScanner(file)
lines []string
hasType bool
scanner = bufio.NewScanner(file)
)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
lines = append(lines, line)
continue
}
if strings.HasPrefix(line, "#") {
lines = append(lines, line)
continue
}
if strings.Contains(line, version) && !a.Recipe.Dirty && !strings.HasSuffix(line, config.DIRTY_DEFAULT) {
skipped = true
lines = append(lines, line)
continue
}
splitted := strings.Split(line, ":")
line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line)
if strings.HasPrefix(line, "TYPE=") {
hasType = true
}
}
if err := scanner.Err(); err != nil {
file.Close()
log.Fatal(err)
}
file.Close()
// The recipe version is carried by a single field: TYPE when present (the
// standard recipe key), otherwise the legacy RECIPE key. When both exist
// (git-URL recipes) RECIPE stays a bare source pointer and only TYPE is
// versioned, so the two can never drift apart.
targetPrefix := "RECIPE="
if hasType {
targetPrefix = "TYPE="
}
for i, line := range lines {
if !strings.HasPrefix(line, targetPrefix) || strings.HasPrefix(line, "#") {
continue
}
name, _, _ := strings.Cut(line, ":")
lines[i] = fmt.Sprintf("%s:%s", name, version)
}
if dryRun {
log.Debug(i18n.G("skipping writing version %s because dry run", version))
return nil
}
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err)
}
if a.Recipe.Dirty && dirtyVersion != "" {
version = dirtyVersion
}
if !dryRun {
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err)
}
} else {
log.Debug(i18n.G("skipping writing version %s because dry run", version))
}
if !skipped {
log.Debug(i18n.G("version %s saved to %s.env", version, a.Domain))
} else {
log.Debug(i18n.G("skipping version %s write as already exists in %s.env", version, a.Domain))
}
log.Debug(i18n.G("version %s saved to %s.env", version, a.Domain))
return nil
}
+56
View File
@@ -3,6 +3,8 @@ package app_test
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
@@ -271,6 +273,60 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
assert.Equal(t, "foo", app.Recipe.EnvVersion)
}
// TestWriteRecipeVersionSingleField ensures the version is written to exactly
// one field: TYPE when present (even alongside a RECIPE source pointer, as with
// git-URL recipes), otherwise the legacy RECIPE field. RECIPE must stay bare
// when a TYPE line exists so the two can never drift apart.
func TestWriteRecipeVersionSingleField(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
{
name: "git recipe: TYPE and RECIPE both present",
input: "TYPE=myrecipe\nRECIPE=git.example.com/user/myrecipe\nDOMAIN=x\n",
want: "TYPE=myrecipe:1.2.3+4.5.6\nRECIPE=git.example.com/user/myrecipe\nDOMAIN=x",
},
{
name: "git recipe: re-versioning replaces only TYPE",
input: "TYPE=myrecipe:0.1.0+1.0.0\nRECIPE=git.example.com/user/myrecipe\nDOMAIN=x\n",
want: "TYPE=myrecipe:1.2.3+4.5.6\nRECIPE=git.example.com/user/myrecipe\nDOMAIN=x",
},
{
name: "legacy recipe: only RECIPE present",
input: "RECIPE=test_recipe\nDOMAIN=x\n",
want: "RECIPE=test_recipe:1.2.3+4.5.6\nDOMAIN=x",
},
{
name: "catalogue recipe: only TYPE present",
input: "TYPE=nextcloud\nDOMAIN=x\n",
want: "TYPE=nextcloud:1.2.3+4.5.6\nDOMAIN=x",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
p := filepath.Join(t.TempDir(), "app.env")
if err := os.WriteFile(p, []byte(tc.input), 0o644); err != nil {
t.Fatal(err)
}
app := appPkg.App{Path: p, Domain: "x"}
if err := app.WriteRecipeVersion("1.2.3+4.5.6", false); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, tc.want, string(got))
})
}
}
func TestWriteRecipeVersionUnknown(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
+30
View File
@@ -0,0 +1,30 @@
package app
import "testing"
func TestInjectRecipeLineAfterType(t *testing.T) {
in := "DOMAIN=example.com\nTYPE=foo\nVERSION=1.0\n"
want := "DOMAIN=example.com\nTYPE=foo\nRECIPE=org/foo\nVERSION=1.0\n"
got := injectRecipeLine(in, "org/foo")
if got != want {
t.Errorf("injectRecipeLine inserted RECIPE in wrong position\nwant:\n%q\ngot:\n%q", want, got)
}
}
func TestInjectRecipeLineReplacesExisting(t *testing.T) {
in := "TYPE=foo\nRECIPE=old\nDOMAIN=example.com\n"
want := "TYPE=foo\nRECIPE=org/foo\nDOMAIN=example.com\n"
got := injectRecipeLine(in, "org/foo")
if got != want {
t.Errorf("injectRecipeLine should replace existing RECIPE line\nwant:\n%q\ngot:\n%q", want, got)
}
}
func TestInjectRecipeLineNoTypeAppends(t *testing.T) {
in := "DOMAIN=example.com\n"
want := "DOMAIN=example.com\nRECIPE=org/foo\n"
got := injectRecipeLine(in, "org/foo")
if got != want {
t.Errorf("injectRecipeLine should append when TYPE is missing\nwant:\n%q\ngot:\n%q", want, got)
}
}
+6
View File
@@ -27,10 +27,16 @@ var BoldUnderlineStyle = lipgloss.NewStyle().
Underline(true)
func ShortenID(str string) string {
if len(str) < 12 {
return str
}
return str[:12]
}
func SmallSHA(hash string) string {
if len(hash) < 8 {
return hash
}
return hash[:8]
}
+5
View File
@@ -36,6 +36,11 @@ func IsClean(repoPath string) (bool, error) {
return false, err
}
// Ignore the abra-managed sidecar file that records the canonical
// source URL for externally-cloned recipes; it lives alongside the
// recipe but is not part of the upstream tree.
patterns = append(patterns, gitignore.ParsePattern(".abra-source", nil))
if len(patterns) > 0 {
worktree.Excludes = append(patterns, worktree.Excludes...)
}
+36
View File
@@ -2,6 +2,8 @@ package git
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/go-git/go-git/v5"
@@ -13,3 +15,37 @@ func TestIsClean(t *testing.T) {
assert.Equal(t, isClean, false)
assert.True(t, errors.Is(err, git.ErrRepositoryNotExists))
}
// TestIsCleanIgnoresAbraSource confirms that the .abra-source sidecar
// file written by abra next to externally-cloned recipes does not cause
// IsClean to report the worktree as dirty.
func TestIsCleanIgnoresAbraSource(t *testing.T) {
dir := t.TempDir()
if _, err := git.PlainInit(dir, false); err != nil {
t.Fatalf("git init failed: %s", err)
}
sidecar := filepath.Join(dir, ".abra-source")
if err := os.WriteFile(sidecar, []byte("git.example.com/u/recipe\n"), 0o644); err != nil {
t.Fatalf("writing sidecar failed: %s", err)
}
isClean, err := IsClean(dir)
if err != nil {
t.Fatalf("IsClean returned error: %s", err)
}
assert.True(t, isClean, "expected worktree with only .abra-source to be reported clean")
// Sanity check: an unrelated untracked file should still mark it dirty.
other := filepath.Join(dir, "random.txt")
if err := os.WriteFile(other, []byte("hello"), 0o644); err != nil {
t.Fatalf("writing extra file failed: %s", err)
}
isClean, err = IsClean(dir)
if err != nil {
t.Fatalf("IsClean returned error: %s", err)
}
assert.False(t, isClean, "expected worktree with unrelated untracked file to be reported dirty")
}
+49
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"path"
"slices"
"sort"
"strings"
@@ -19,6 +20,11 @@ import (
"github.com/go-git/go-git/v5/plumbing"
)
// SourceFile is the sidecar file written next to an externally-cloned
// recipe so the canonical "host/path" name can be recovered later (the
// on-disk directory name escapes "/" and "." to "_", which is lossy).
const SourceFile = ".abra-source"
type EnsureContext struct {
Chaos bool
Offline bool
@@ -78,6 +84,12 @@ func (r Recipe) EnsureExists() error {
if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil {
return err
}
if strings.Contains(r.Name, "/") {
sidecar := path.Join(r.Dir, SourceFile)
if err := os.WriteFile(sidecar, []byte(r.Name+"\n"), 0o644); err != nil {
log.Debug(i18n.G("failed to write recipe source sidecar %s: %s", sidecar, err))
}
}
}
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
@@ -87,6 +99,18 @@ func (r Recipe) EnsureExists() error {
return nil
}
// ReadRecipeSource returns the canonical name recorded in the .abra-source
// sidecar inside the given recipe directory, or the empty string if no
// sidecar exists. This lets callers recover the unescaped "host/path"
// form for externally-cloned recipes.
func ReadRecipeSource(recipeDir string) string {
data, err := os.ReadFile(path.Join(recipeDir, SourceFile))
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
// IsChaosCommit determines if a version sttring is a chaos commit or not.
func (r Recipe) IsChaosCommit(version string) (bool, error) {
isChaosCommit := false
@@ -163,6 +187,9 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
hash, err := repo.ResolveRevision(plumbing.Revision(version))
if err != nil {
if isRemoteBranch(repo, version) {
log.Fatal(i18n.G("'%s' is a branch name; ':<version>' only supports tags or commit hashes, not branches", version))
}
log.Fatal(i18n.G("unable to resolve '%s': %s", version, err))
}
@@ -479,6 +506,28 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
return versions, uniqueWarnings, nil
}
// isRemoteBranch reports whether name matches a branch on any configured
// remote. Used to give a clearer error when a user passes a branch as the
// ":version" suffix, which is unsupported.
func isRemoteBranch(repo *git.Repository, name string) bool {
remotes, err := repo.Remotes()
if err != nil {
return false
}
for _, remote := range remotes {
refs, err := remote.List(&git.ListOptions{})
if err != nil {
continue
}
for _, ref := range refs {
if ref.Name().IsBranch() && ref.Name().Short() == name {
return true
}
}
}
return false
}
// Head retrieves latest HEAD metadata.
func (r Recipe) Head() (*plumbing.Reference, error) {
repo, err := git.PlainOpen(r.Dir)
+88
View File
@@ -8,6 +8,7 @@ import (
"net/url"
"os"
"path"
"regexp"
"sort"
"strconv"
"strings"
@@ -121,7 +122,84 @@ type Features struct {
SSO string `json:"sso"`
}
// scpURLPattern matches SCP-style git URLs like git@host:path or git@host:port/path.
// Captures: 1=host(:port)?, 2=path.
var scpURLPattern = regexp.MustCompile(`^[\w.-]+@([\w.-]+(?::\d+)?):(.+)$`)
// NormalizeRecipeName canonicalizes a recipe identifier to a stable
// "host/path" form (or returns short catalog names unchanged). Accepts:
//
// - https://host/path[.git][:version]
// - http://host/path[.git][:version]
// - ssh://git@host[:port]/path[.git][:version]
// - git@host:path[.git][:version] (SCP-style)
// - host/path[.git][:version] (already canonical)
// - short-name[:version] (catalog recipe, pass through)
//
// The optional trailing :version suffix is preserved verbatim. The .git
// suffix and trailing slashes on the path are stripped so the four URL
// forms of the same repository collapse to one canonical value.
func NormalizeRecipeName(input string) string {
input = strings.TrimSpace(input)
// Split off the version suffix first, but only if it's not part of a
// scheme (https://) or SCP-style git@host: prefix. The simplest way is
// to detect those prefixes and treat the rest as the path-with-version.
var (
body = input
version string
)
switch {
case strings.HasPrefix(input, "https://") || strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "ssh://"):
u, err := url.Parse(input)
if err != nil {
return input
}
host := u.Hostname() // strip port to keep canonical form colon-free
p := strings.TrimPrefix(u.Path, "/")
p, version = splitVersion(p)
body = host + "/" + p
case scpURLPattern.MatchString(input):
m := scpURLPattern.FindStringSubmatch(input)
host := m[1]
if i := strings.Index(host, ":"); i >= 0 {
host = host[:i] // strip port
}
p, v := splitVersion(m[2])
body = host + "/" + p
version = v
default:
body, version = splitVersion(input)
}
body = strings.TrimSuffix(body, "/")
body = strings.TrimSuffix(body, ".git")
if version != "" {
return body + ":" + version
}
return body
}
// splitVersion separates a trailing ":version" suffix from a recipe path.
// It only treats the final colon-separated segment as a version when it is
// non-empty and contains no slash (a slash means the colon belonged to the
// path, e.g. a host:port or scheme separator).
func splitVersion(s string) (string, string) {
idx := strings.LastIndex(s, ":")
if idx < 0 {
return s, ""
}
candidate := s[idx+1:]
if candidate == "" || strings.Contains(candidate, "/") {
return s, ""
}
return s[:idx], candidate
}
func Get(name string) Recipe {
name = NormalizeRecipeName(name)
version := ""
versionRaw := ""
if strings.Contains(name, ":") {
@@ -209,6 +287,16 @@ func (r Recipe) String() string {
return out
}
// ShortName returns the final path segment of the recipe name, i.e. the
// bare recipe name without any "host/org/" prefix carried by externally
// sourced (git URL) recipes. For catalogue recipes it returns Name unchanged.
func (r Recipe) ShortName() string {
if i := strings.LastIndex(r.Name, "/"); i >= 0 {
return r.Name[i+1:]
}
return r.Name
}
func escapeRecipeName(recipeName string) string {
recipeName = strings.ReplaceAll(recipeName, "/", "_")
recipeName = strings.ReplaceAll(recipeName, ".", "_")
+125
View File
@@ -88,6 +88,88 @@ func TestGet(t *testing.T) {
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "https://mygit.org/myorg/cool-recipe",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "https://mygit.org/myorg/cool-recipe.git",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "https://mygit.org/myorg/cool-recipe.git:1.2.4",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
EnvVersion: "1.2.4",
EnvVersionRaw: "1.2.4",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "ssh://git@mygit.org/myorg/cool-recipe.git",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "git@mygit.org:myorg/cool-recipe.git",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "git@mygit.org:myorg/cool-recipe:1.2.4",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
EnvVersion: "1.2.4",
EnvVersionRaw: "1.2.4",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
}
for _, tc := range testcases {
@@ -101,6 +183,49 @@ func TestGet(t *testing.T) {
}
}
func TestNormalizeRecipeName(t *testing.T) {
cases := []struct {
in, want string
}{
// catalog short names pass through
{"foo", "foo"},
{"foo:1.2.3", "foo:1.2.3"},
// bare host/path form
{"mygit.org/myorg/cool-recipe", "mygit.org/myorg/cool-recipe"},
{"mygit.org/myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
{"mygit.org/myorg/cool-recipe.git:1.2.3", "mygit.org/myorg/cool-recipe:1.2.3"},
{"mygit.org/myorg/cool-recipe/", "mygit.org/myorg/cool-recipe"},
// https://
{"https://mygit.org/myorg/cool-recipe", "mygit.org/myorg/cool-recipe"},
{"https://mygit.org/myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
{"https://mygit.org/myorg/cool-recipe.git:1.2.3", "mygit.org/myorg/cool-recipe:1.2.3"},
{"http://mygit.org/myorg/cool-recipe", "mygit.org/myorg/cool-recipe"},
// ssh://, with and without port
{"ssh://git@mygit.org/myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
{"ssh://git@mygit.org:2222/myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
// SCP-style git@host:path
{"git@mygit.org:myorg/cool-recipe", "mygit.org/myorg/cool-recipe"},
{"git@mygit.org:myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
{"git@mygit.org:myorg/cool-recipe:1.2.3", "mygit.org/myorg/cool-recipe:1.2.3"},
// whitespace
{" https://mygit.org/myorg/cool-recipe ", "mygit.org/myorg/cool-recipe"},
}
for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
got := NormalizeRecipeName(tc.in)
if got != tc.want {
t.Errorf("NormalizeRecipeName(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.2
image: nginx:1.31.3
secrets:
- test_pass_one
- test_pass_two
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.2
image: nginx:1.31.3
networks:
- proxy
deploy:
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609
FROM golang:1.26@sha256:b900de91b15b2e2953d930ece1d0ecff0a1590ab2006088d20dcf0f56f1e979f
ENV GOOS=linux
ENV GOARCH=arm
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26@sha256:f96cc555eb8db430159a3aa6797cd5bae561945b7b0fe7d0e284c63a3b291609
FROM golang:1.26@sha256:b900de91b15b2e2953d930ece1d0ecff0a1590ab2006088d20dcf0f56f1e979f
ENV GOOS=linux
ENV GOARCH=arm64
+1 -1
View File
@@ -1 +1 @@
codespell==2.4.2
codespell==2.4.3
+37 -25
View File
@@ -6,38 +6,50 @@ package cpu
import "strconv"
// parseRelease parses a dot-separated version number. It follows the semver
// syntax, but allows the minor and patch versions to be elided.
// parseRelease parses a dot-separated version number from the prefix
// of rel. It returns ok=true only if at least the major and minor
// components were successfully parsed; the patch component is
// best-effort. Trailing vendor or build suffixes such as
// "-generic", "+", "_hi3535", or "-rc1" are ignored.
//
// This is a copy of the Go runtime's parseRelease from
// https://golang.org/cl/209597.
// https://golang.org/cl/209597, updated in https://golang.org/cl/781800.
func parseRelease(rel string) (major, minor, patch int, ok bool) {
// Strip anything after a dash or plus.
for i := range len(rel) {
if rel[i] == '-' || rel[i] == '+' {
rel = rel[:i]
break
// next consumes a run of decimal digits from the front of rel,
// returning the parsed value. If the digits are followed by a
// '.', it is consumed and more is set so the caller knows to
// parse another component; otherwise scanning terminates and
// the rest of rel is discarded.
next := func() (n int, more, ok bool) {
i := 0
for i < len(rel) && rel[i] >= '0' && rel[i] <= '9' {
i++
}
if i == 0 {
return 0, false, false
}
n, err := strconv.Atoi(rel[:i])
if err != nil {
return 0, false, false
}
if i < len(rel) && rel[i] == '.' {
rel = rel[i+1:]
return n, true, true
}
rel = ""
return n, false, true
}
next := func() (int, bool) {
for i := range len(rel) {
if rel[i] == '.' {
ver, err := strconv.Atoi(rel[:i])
rel = rel[i+1:]
return ver, err == nil
}
}
ver, err := strconv.Atoi(rel)
rel = ""
return ver, err == nil
var more bool
if major, more, ok = next(); !ok || !more {
return 0, 0, 0, false
}
if major, ok = next(); !ok || rel == "" {
return
if minor, more, ok = next(); !ok {
return 0, 0, 0, false
}
if minor, ok = next(); !ok || rel == "" {
return
if !more {
return major, minor, 0, true
}
patch, ok = next()
return
patch, _, _ = next()
return major, minor, patch, true
}
+1
View File
@@ -1874,6 +1874,7 @@ func Dup2(oldfd, newfd int) error {
//sys Dup3(oldfd int, newfd int, flags int) (err error)
//sysnb EpollCreate1(flag int) (fd int, err error)
//sysnb EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT
//sys Eventfd(initval uint, flags int) (fd int, err error) = SYS_EVENTFD2
//sys Exit(code int) = SYS_EXIT_GROUP
//sys Fallocate(fd int, mode uint32, off int64, len int64) (err error)
-1
View File
@@ -20,7 +20,6 @@ func setTimeval(sec, usec int64) Timeval {
// 64-bit file system and 32-bit uid calls
// (386 default is 32-bit file system and 16-bit uid).
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64_64
//sys Fchown(fd int, uid int, gid int) (err error) = SYS_FCHOWN32
//sys Fstat(fd int, stat *Stat_t) (err error) = SYS_FSTAT64
-1
View File
@@ -6,7 +6,6 @@
package unix
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -44,7 +44,6 @@ func Seek(fd int, offset int64, whence int) (newoffset int64, err error) {
// 64-bit file system and 32-bit uid calls
// (16-bit uid calls are not always supported in newer kernels)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fchown(fd int, uid int, gid int) (err error) = SYS_FCHOWN32
//sys Fstat(fd int, stat *Stat_t) (err error) = SYS_FSTAT64
//sys Fstatat(dirfd int, path string, stat *Stat_t, flags int) (err error) = SYS_FSTATAT64
-1
View File
@@ -8,7 +8,6 @@ package unix
import "unsafe"
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -8,7 +8,6 @@ package unix
import "unsafe"
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstatfs(fd int, buf *Statfs_t) (err error)
-1
View File
@@ -6,7 +6,6 @@
package unix
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstatfs(fd int, buf *Statfs_t) (err error)
-1
View File
@@ -13,7 +13,6 @@ import (
func Syscall9(trap, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err syscall.Errno)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Ftruncate(fd int, length int64) (err error) = SYS_FTRUNCATE64
-1
View File
@@ -11,7 +11,6 @@ import (
"unsafe"
)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error) = SYS_FSTAT64
//sys Fstatat(dirfd int, path string, stat *Stat_t, flags int) (err error) = SYS_FSTATAT64
-1
View File
@@ -6,7 +6,6 @@
package unix
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -8,7 +8,6 @@ package unix
import "unsafe"
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) = SYS_EPOLL_PWAIT
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -10,7 +10,6 @@ import (
"unsafe"
)
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
-1
View File
@@ -6,7 +6,6 @@
package unix
//sys EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
//sys Fadvise(fd int, offset int64, length int64, advice int) (err error) = SYS_FADVISE64
//sys Fchown(fd int, uid int, gid int) (err error)
//sys Fstat(fd int, stat *Stat_t) (err error)
+7 -1
View File
@@ -1359,6 +1359,7 @@ const (
FAN_UNLIMITED_MARKS = 0x20
FAN_UNLIMITED_QUEUE = 0x10
FD_CLOEXEC = 0x1
FD_PIDFS_ROOT = -0x2712
FD_SETSIZE = 0x400
FF0 = 0x0
FIB_RULE_DEV_DETACHED = 0x8
@@ -1970,6 +1971,8 @@ const (
MADV_DONTNEED = 0x4
MADV_DONTNEED_LOCKED = 0x18
MADV_FREE = 0x8
MADV_GUARD_INSTALL = 0x66
MADV_GUARD_REMOVE = 0x67
MADV_HUGEPAGE = 0xe
MADV_HWPOISON = 0x64
MADV_KEEPONFORK = 0x13
@@ -2114,7 +2117,7 @@ const (
MS_NOSEC = 0x10000000
MS_NOSUID = 0x2
MS_NOSYMFOLLOW = 0x100
MS_NOUSER = -0x80000000
MS_NOUSER = 0x80000000
MS_POSIXACL = 0x10000
MS_PRIVATE = 0x40000
MS_RDONLY = 0x1
@@ -3786,6 +3789,9 @@ const (
TCPOPT_TIMESTAMP = 0x8
TCPOPT_TSTAMP_HDR = 0x101080a
TCPOPT_WINDOW = 0x3
TCP_AO_KEYF_EXCLUDE_OPT = 0x2
TCP_AO_KEYF_IFINDEX = 0x1
TCP_AO_MAXKEYLEN = 0x50
TCP_CC_INFO = 0x1a
TCP_CM_INQ = 0x24
TCP_CONGESTION = 0xd
+17
View File
@@ -700,6 +700,23 @@ func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Eventfd(initval uint, flags int) (fd int, err error) {
r0, _, e1 := Syscall(SYS_EVENTFD2, uintptr(initval), uintptr(flags), 0)
fd = int(r0)
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64_64, uintptr(fd), uintptr(offset), uintptr(offset>>32), uintptr(length), uintptr(length>>32), uintptr(advice))
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -213,23 +213,6 @@ func sendmsg(s int, msg *Msghdr, flags int) (n int, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fchown(fd int, uid int, gid int) (err error) {
_, _, e1 := Syscall(SYS_FCHOWN32, uintptr(fd), uintptr(uid), uintptr(gid))
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall9(SYS_FADVISE64, uintptr(fd), 0, uintptr(offset>>32), uintptr(offset), uintptr(length>>32), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall9(SYS_FADVISE64, uintptr(fd), 0, uintptr(offset), uintptr(offset>>32), uintptr(length), uintptr(length>>32), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fchown(fd int, uid int, gid int) (err error) {
_, _, e1 := Syscall(SYS_FCHOWN, uintptr(fd), uintptr(uid), uintptr(gid))
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
-17
View File
@@ -45,23 +45,6 @@ func Tee(rfd int, wfd int, len int, flags int) (n int64, err error) {
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error) {
var _p0 unsafe.Pointer
if len(events) > 0 {
_p0 = unsafe.Pointer(&events[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall6(SYS_EPOLL_WAIT, uintptr(epfd), uintptr(_p0), uintptr(len(events)), uintptr(msec), 0, 0)
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func Fadvise(fd int, offset int64, length int64, advice int) (err error) {
_, _, e1 := Syscall6(SYS_FADVISE64, uintptr(fd), uintptr(offset), uintptr(length), uintptr(advice), 0, 0)
if e1 != 0 {
+36
View File
@@ -1109,17 +1109,53 @@ const (
)
// This type is the union inside of TRUSTEE and must be created using one of the TrusteeValueFrom* functions.
//
// Go pointers stored in a TrusteeValue must be pinned using [runtime.Pinner]
// for the lifetime of the TrusteeValue.
type TrusteeValue uintptr
// TrusteeValueFromString is unsafe and should not be used.
//
// It returns a uintptr containing a reference to newly-allocated memory
// which will be freed by the garbage collector.
// There is no way for the caller to safely reference this memory.
//
// To create a [TrusteeValue] from a string, use:
//
// p, err := windows.UTF16PtrFromString(s)
// if err != nil {
// // handle error
// }
//
// // Pin the string for as long as it is used.
// var pinner runtime.Pinner
// pinner.Pin(p)
// defer pinner.Unpin()
//
// tv := TrusteeValue(unsafe.Pointer(p))
//
// Deprecated: TrusteeValueFromString is unsafe and should not be used.
func TrusteeValueFromString(str string) TrusteeValue {
return TrusteeValue(unsafe.Pointer(StringToUTF16Ptr(str)))
}
// TrusteeValueFromSID returns a [TrusteeValue] referencing sid.
//
// The caller must pin sid using a [runtime.Pinner] for the lifetime of the TrusteeValue.
func TrusteeValueFromSID(sid *SID) TrusteeValue {
return TrusteeValue(unsafe.Pointer(sid))
}
// TrusteeValueFromObjectsAndSid returns a [TrusteeValue] referencing objectsAndSid.
//
// The caller must pin objectsAndSid using a [runtime.Pinner] for the lifetime of the TrusteeValue.
func TrusteeValueFromObjectsAndSid(objectsAndSid *OBJECTS_AND_SID) TrusteeValue {
return TrusteeValue(unsafe.Pointer(objectsAndSid))
}
// TrusteeValueFromObjectsAndName returns a [TrusteeValue] referencing objectsAndName.
//
// The caller must pin objectsAndName using a [runtime.Pinner] for the lifetime of the TrusteeValue.
func TrusteeValueFromObjectsAndName(objectsAndName *OBJECTS_AND_NAME) TrusteeValue {
return TrusteeValue(unsafe.Pointer(objectsAndName))
}
+6 -2
View File
@@ -1728,11 +1728,15 @@ func (s *NTUnicodeString) String() string {
// the more common *uint16 string type.
func NewNTString(s string) (*NTString, error) {
var nts NTString
s8, err := BytePtrFromString(s)
s8, err := ByteSliceFromString(s)
if err != nil {
return nil, err
}
RtlInitString(&nts, s8)
// The source string plus its terminating NUL must fit within MAX_USHORT.
if len(s8) > MAX_USHORT {
return nil, syscall.EINVAL
}
RtlInitString(&nts, &s8[0])
return &nts, nil
}
+1
View File
@@ -169,6 +169,7 @@ const (
FORMAT_MESSAGE_ARGUMENT_ARRAY = 8192
FORMAT_MESSAGE_MAX_WIDTH_MASK = 255
MAX_USHORT = 0xffff
MAX_PATH = 260
MAX_LONG_PATH = 32768
+2 -2
View File
@@ -700,7 +700,7 @@ golang.org/x/net/internal/socks
golang.org/x/net/internal/timeseries
golang.org/x/net/proxy
golang.org/x/net/trace
# golang.org/x/sys v0.46.0
# golang.org/x/sys v0.47.0
## explicit; go 1.25.0
golang.org/x/sys/cpu
golang.org/x/sys/execabs
@@ -708,7 +708,7 @@ golang.org/x/sys/plan9
golang.org/x/sys/unix
golang.org/x/sys/windows
golang.org/x/sys/windows/registry
# golang.org/x/term v0.44.0
# golang.org/x/term v0.45.0
## explicit; go 1.25.0
golang.org/x/term
# golang.org/x/text v0.36.0