From 46cc1607645da5ca29f195d0646436a79940b810 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Sun, 18 Sep 2022 17:02:47 +0200 Subject: [PATCH 1/2] Examples, rename and fixes updated roadmap updated roadmap Add a basic readme (#15) Co-authored-by: kjuulh Reviewed-on: https://git.front.kjuulh.io/kjuulh/kraken/pulls/15 fix readme Add readme with stuff more roadmap items update formatting formatting 2 more setup with semantic with semantic 2 revert add releaserc with releaser with kraken Update roadmap rename rename feature/move-command (#18) Co-authored-by: kjuulh Reviewed-on: https://git.front.kjuulh.io/kjuulh/octopush/pulls/18 fix/require-two-pushes (#20) Co-authored-by: kjuulh Reviewed-on: https://git.front.kjuulh.io/kjuulh/octopush/pulls/20 --- CONFIGURATION_SERVER.md | 1 + README.md | 249 +++++++++++++ _examples/actions/add_releaserc/go.mod | 11 + _examples/actions/add_releaserc/go.sum | 20 ++ _examples/actions/add_releaserc/main.go | 25 ++ _examples/actions/add_releaserc/octopush.yml | 12 + _examples/actions/docker_action/Dockerfile | 2 +- .../{kraken.yml => octopush.yml} | 4 +- .../{kraken.yml => octopush.yml} | 4 +- _examples/queries/find_semantic/octopush.yml | 9 + .../{kraken.yml => octopush.yml} | 4 +- assets/octopush.png | Bin 0 -> 32918 bytes assets/octopush.svg | 10 + cmd/kraken/commands/root.go | 14 - cmd/kraken/kraken.go | 18 - cmd/octopush/commands/process.go | 61 ++++ cmd/octopush/commands/root.go | 18 + .../commands/server}/process.go | 4 +- cmd/octopush/commands/server/server.go | 16 + cmd/octopush/octopush.go | 28 ++ cmd/server/commands/root.go | 2 +- cmd/server/commands/start.go | 4 +- cmd/server/server.go | 4 +- cuddle.yaml | 2 +- go.mod | 14 +- go.sum | 23 +- integration_test/main_test.go | 2 +- integration_test/storage_test.go | 2 +- internal/actions/action.go | 10 +- internal/actions/action_creator.go | 16 +- internal/actions/builders/docker.go | 4 +- internal/api/process_command.go | 6 +- internal/api/root.go | 2 +- internal/cli/cli.go | 30 ++ internal/commands/process_repos.go | 25 +- internal/logger/zap.go | 3 +- internal/schema/kraken.go | 6 +- internal/server/http_server.go | 4 +- internal/server/server.go | 4 +- internal/server/storage_server.go | 2 +- internal/serverdeps/server_deps.go | 14 +- internal/services/actions/action.go | 2 +- internal/services/providers/git.go | 338 ----------------- internal/services/providers/gogit.go | 339 ++++++++++++++++++ internal/services/storage/storage.go | 2 +- roadmap.md | 32 +- scripts/push_github.sh | 2 +- scripts/run_client.sh | 9 +- 48 files changed, 934 insertions(+), 479 deletions(-) create mode 100644 CONFIGURATION_SERVER.md create mode 100644 README.md create mode 100644 _examples/actions/add_releaserc/go.mod create mode 100644 _examples/actions/add_releaserc/go.sum create mode 100644 _examples/actions/add_releaserc/main.go create mode 100644 _examples/actions/add_releaserc/octopush.yml rename _examples/actions/docker_action/{kraken.yml => octopush.yml} (62%) rename _examples/actions/write_a_readme/{kraken.yml => octopush.yml} (60%) create mode 100644 _examples/queries/find_semantic/octopush.yml rename _examples/queries/scrape_readme/{kraken.yml => octopush.yml} (61%) create mode 100644 assets/octopush.png create mode 100644 assets/octopush.svg delete mode 100644 cmd/kraken/commands/root.go delete mode 100644 cmd/kraken/kraken.go create mode 100644 cmd/octopush/commands/process.go create mode 100644 cmd/octopush/commands/root.go rename cmd/{kraken/commands => octopush/commands/server}/process.go (95%) create mode 100644 cmd/octopush/commands/server/server.go create mode 100644 cmd/octopush/octopush.go create mode 100644 internal/cli/cli.go create mode 100644 internal/services/providers/gogit.go diff --git a/CONFIGURATION_SERVER.md b/CONFIGURATION_SERVER.md new file mode 100644 index 0000000..04d5c69 --- /dev/null +++ b/CONFIGURATION_SERVER.md @@ -0,0 +1 @@ +# Configuration server diff --git a/README.md b/README.md new file mode 100644 index 0000000..10ea947 --- /dev/null +++ b/README.md @@ -0,0 +1,249 @@ +

+ +

+

Octopush - Your cute action executor

+ +## Purpose + +The goal of this project is to easily do batch changes or queries on a host of +repositories. In large organisations using multi-repository strategies, it may +be painful to change even small things across many repositories, because there +are so many of them. Octopush aims to change that. + +**DISCLAIMER:** It is still early days, and the api is subject to change. + +## Features + +- Uses an actions repository, where you store all your pending commands or + queries to be performed across your fleet of repositories. (See \_examples) +- Actions can both execute changes, open pull-requests or in some cases commit + directly to your preferred branch + - Actions natively use either shell, go or docker files to execute changes + (see \_examples/actions) +- Actions can also be analytical, so you can query your fleet for whatever you + would like +- Works both as a client, or as a server +- Supports SSH/https for fetching repos +- Supports GPG signing +- Supports dry-run mode for easy testing when developing your actions (enabled + by default on the cli) + +## Roadmap + +Refer to [roadmap.md](roadmap.md) + +## Installation + +Octopush comes in two modes. Client or Client -> Server. Octopush can stand alone as +a client, for smaller and less secure changes. However, for organisations, it +may be useful to use Octopush in server mode, which supports more features, and +has extra security built in. + +### Client (CLI) + +Download executable from [releases](https://github.com/kjuulh/octopush/releases) + +#### Or Use docker image + +```bash +docker run --rm kasperhermansen/octopushcli:latest version +``` + +#### Or Build from source + +```bash +git clone https://github.com/kjuulh/octopush.git +cd octopush + +go build cmd/octopush/octopush.go +./octopush version +``` + +#### Or Build with cuddle + +```bash +git clone https://github.com/kjuulh/octopush.git +cd octopush + +cuddle_cli x build_cli +``` + +### Server + +We prefer to run the server directly as a docker image. + +```bash +docker pull kasperhermansen/octopushserver:latest +docker run -p 9090:80 --rm kasperhermansen/octopushserver:latest +``` + +#### Or Build from source + +```bash +git clone https://github.com/kjuulh/octopush.git +cd octopush + +go build cmd/server/server.go +./server version +``` + +#### Or Build with cuddle + +```bash +git clone https://github.com/kjuulh/octopush.git +cd octopush + +cuddle_cli x build_server +``` + +## Usage + +**DISCLAIMER:** It is still early days, and the api of the CLI is subject to +change, this provides the aim of the project, but as it is currently in flux, +there may not be as much handholding in the actual usage. + +I will focus on the client here, as the server provides the same features, +though available through the cli, but instead as configuration options (see +[CONFIGURATION_SERVER.md](CONFIGURATION_SERVER.md)) + +Octopush ships with autocomplete built in (courtesy of spf13/cobra). To add: + +- Bash: `echo 'source <(octopush completion bash)' >> ~/.bashrc` +- Zsh: `echo 'source <(octopush completion zsh)' >> ~/.zshrc` + +### Creating a new action + +Creating a new action + +```bash +git init my-actions # should only be done once +cd my-actions +octopush tmpl init write-a-readme --command +cat write-a-readme/octopush.yml + +# Output +# apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 +# name: write-a-readme +# select: +# repositories: [] +# actions: +# - type: shell +# entry: "main.sh" +``` + +Octopush also ships with yaml schema, which should help write the yaml +configuration. + +#### Add upstream repositories (victims) + +Now add a preferred repository + +``` +cat << EOF > write-a-readme/octopush.yml +apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 +name: write-a-readme +select: + providers: # new + - gitea: https://git.front.kjuulh.io # new + organisation: "kjuulh" # new +actions: + - type: shell + entry: "main.sh" +EOF +``` + +This will take all your repositories under an organisation and run the script +on. + +Another could be to use + +```bash +cat << EOF > write-a-readme/octopush.yml +apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 +name: write-a-readme +select: + repositories: #new + - git@git.front.kjuulh.io:kjuulh/octopush.git #new + - git@git.front.kjuulh.io:kjuulh/octopush-test.git #new +actions: + - type: shell + entry: "main.sh" +EOF +``` + +This will just apply to those repositories instead. Both can also be combined +for a shared effect. + +### Execute action + +To run the script use + +```bash +octopush process --path "write-a-readme" +``` + +This will cause the octopush process to automatically apply the action on the repo +and open a pr. + +### Query repositories + +Octopush can also be used to query. + +```bash +cat << EOF > write-a-readme/octopush.yml +apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 +name: write-a-readme +select: + repositories: + - git@git.front.kjuulh.io:kjuulh/octopush.git + - git@git.front.kjuulh.io:kjuulh/octopush-test.git +queries: + - type: grep + query: "# README" +EOF +``` + +Using the same command as above, will return the lines on each repo with those +criteria. Everything is run in docker, even locally, so no need to install fancy +tools. + +Do note: All actions will be run as dry-run unless `--apply` is added. This is +to help test locally, as well as not cause serious issues. The server +configuration is pretty much the same, except the command would look like so: +`octopush server process --path "write-a-readme" --apply`. Octopush will try to +infer as much as possible, but it may be needed to apply some extra flags to +specify upstream repositories and such. Octopush will also help you setup keys and +such on the first run, using `octopush setup` or `octopush server setup`. + +## Contributing + +It is still early days, and as such things are moving fast, I may not be able to +implement features, because I am focusing my energy on the API. That said PRs +are welcome, though they are at your own risk. + +### Bugs & features requests + +Please use [issues](https://github.com/kjuulh/octopush/issues) + +### Development + +We use [cuddle](https://git.front.kjuulh.io/kjuulh/cuddle) to improve ease of +use, it is however, not a requirement, and probably won't need to be used +outside core maintainers. + +Simply: + +```bash +go run cmd/octopush/octopush.go # CLI +go run cmd/server/server.go # Server +``` + +We follow the `gofmt` formatting, along with optionally but recommend `golines` + +If using cuddle + +``` +cuddle_cli x run # Run both server and client, will do a quick test sweep on the cli +cuddle_cli x watch_run # Automatically refresh both +cuddle_cli x fmt # will format the current code +``` diff --git a/_examples/actions/add_releaserc/go.mod b/_examples/actions/add_releaserc/go.mod new file mode 100644 index 0000000..248ba10 --- /dev/null +++ b/_examples/actions/add_releaserc/go.mod @@ -0,0 +1,11 @@ +module write_a_readme + +go 1.19 + +require github.com/bitfield/script v0.20.2 + +require ( + bitbucket.org/creachadair/shell v0.0.7 // indirect + github.com/itchyny/gojq v0.12.7 // indirect + github.com/itchyny/timefmt-go v0.1.3 // indirect +) diff --git a/_examples/actions/add_releaserc/go.sum b/_examples/actions/add_releaserc/go.sum new file mode 100644 index 0000000..234eb1e --- /dev/null +++ b/_examples/actions/add_releaserc/go.sum @@ -0,0 +1,20 @@ +bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= +bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= +github.com/bitfield/script v0.20.2 h1:4DexsRtBILVMEn3EZwHbtJdDqdk43sXI8gM3F04JXgs= +github.com/bitfield/script v0.20.2/go.mod h1:l3AZPVAtKQrL03bwh7nlNTUtgrgSWurpJSbtqspYrOA= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/itchyny/gojq v0.12.7 h1:hYPTpeWfrJ1OT+2j6cvBScbhl0TkdwGM4bc66onUSOQ= +github.com/itchyny/gojq v0.12.7/go.mod h1:ZdvNHVlzPgUf8pgjnuDTmGfHA/21KoutQUJ3An/xNuw= +github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU= +github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/actions/add_releaserc/main.go b/_examples/actions/add_releaserc/main.go new file mode 100644 index 0000000..9ff20ba --- /dev/null +++ b/_examples/actions/add_releaserc/main.go @@ -0,0 +1,25 @@ +package main + +import "github.com/bitfield/script" + +func main() { + + releaseRc := ` + branches: + - "main" + - "v0.x" + + plugins: + - "@semantic-release/commit-analyzer" + - "@semantic-release/release-notes-generator" + - "@semantic-release/changelog" + - "@semantic-release/git" + ` + + _, err := script. + Echo(releaseRc). + WriteFile(".releaserc.yml") + if err != nil { + panic(err) + } +} diff --git a/_examples/actions/add_releaserc/octopush.yml b/_examples/actions/add_releaserc/octopush.yml new file mode 100644 index 0000000..d90c930 --- /dev/null +++ b/_examples/actions/add_releaserc/octopush.yml @@ -0,0 +1,12 @@ +apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 +name: write-a-readme +select: + repositories: + - git@git.front.kjuulh.io:kjuulh/octopush-test.git + #- git@git.front.kjuulh.io:kjuulh/octopush.git + # providers: + # - gitea: https://git.front.kjuulh.io + # organisation: "cibus" +actions: + - type: go + entry: "main.go" diff --git a/_examples/actions/docker_action/Dockerfile b/_examples/actions/docker_action/Dockerfile index 572de73..61980fa 100644 --- a/_examples/actions/docker_action/Dockerfile +++ b/_examples/actions/docker_action/Dockerfile @@ -1,6 +1,6 @@ FROM debian:bullseye-slim -# Kraken relies on this path being the specified path +# Octopush relies on this path being the specified path WORKDIR /src/work/ COPY entry.sh /src/script.sh diff --git a/_examples/actions/docker_action/kraken.yml b/_examples/actions/docker_action/octopush.yml similarity index 62% rename from _examples/actions/docker_action/kraken.yml rename to _examples/actions/docker_action/octopush.yml index f2c8dba..4279c59 100644 --- a/_examples/actions/docker_action/kraken.yml +++ b/_examples/actions/docker_action/octopush.yml @@ -1,8 +1,8 @@ -apiVersion: git.front.kjuulh.io/kjuulh/kraken/blob/main/schema/v1 +apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 name: write-a-readme select: repositories: - - git@git.front.kjuulh.io:kjuulh/kraken-test.git + - git@git.front.kjuulh.io:kjuulh/octopush-test.git # providers: # - gitea: https://git.front.kjuulh.io # organisation: "cibus" diff --git a/_examples/actions/write_a_readme/kraken.yml b/_examples/actions/write_a_readme/octopush.yml similarity index 60% rename from _examples/actions/write_a_readme/kraken.yml rename to _examples/actions/write_a_readme/octopush.yml index 0e5c44e..a4bf81e 100644 --- a/_examples/actions/write_a_readme/kraken.yml +++ b/_examples/actions/write_a_readme/octopush.yml @@ -1,8 +1,8 @@ -apiVersion: git.front.kjuulh.io/kjuulh/kraken/blob/main/schema/v1 +apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 name: write-a-readme select: repositories: - - git@git.front.kjuulh.io:kjuulh/kraken-test.git + - git@git.front.kjuulh.io:kjuulh/octopush-test.git # providers: # - gitea: https://git.front.kjuulh.io # organisation: "cibus" diff --git a/_examples/queries/find_semantic/octopush.yml b/_examples/queries/find_semantic/octopush.yml new file mode 100644 index 0000000..6952292 --- /dev/null +++ b/_examples/queries/find_semantic/octopush.yml @@ -0,0 +1,9 @@ +apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 +name: write-a-readme +select: + providers: + - gitea: https://git.front.kjuulh.io + organisation: "cibus" +queries: + - type: grep + query: "releaser" diff --git a/_examples/queries/scrape_readme/kraken.yml b/_examples/queries/scrape_readme/octopush.yml similarity index 61% rename from _examples/queries/scrape_readme/kraken.yml rename to _examples/queries/scrape_readme/octopush.yml index 35aa3a3..f1fe9ff 100644 --- a/_examples/queries/scrape_readme/kraken.yml +++ b/_examples/queries/scrape_readme/octopush.yml @@ -1,8 +1,8 @@ -apiVersion: git.front.kjuulh.io/kjuulh/kraken/blob/main/schema/v1 +apiVersion: git.front.kjuulh.io/kjuulh/octopush/blob/main/schema/v1 name: write-a-readme select: repositories: - - git@git.front.kjuulh.io:kjuulh/kraken-test.git + - git@git.front.kjuulh.io:kjuulh/octopush-test.git # providers: # - gitea: https://git.front.kjuulh.io # organisation: "cibus" diff --git a/assets/octopush.png b/assets/octopush.png new file mode 100644 index 0000000000000000000000000000000000000000..9d1c4993c197ddc2e6527e21e4cb01caa5ca3438 GIT binary patch literal 32918 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

%xgl<&daem_ei*#AvN$&rIJO7M?<+M! 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 WPnyP`2RU5+r>c_HGsIJ?(EktLL<~6q literal 0 HcmV?d00001 diff --git a/assets/octopush.svg b/assets/octopush.svg new file mode 100644 index 0000000..8812c64 --- /dev/null +++ b/assets/octopush.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/cmd/kraken/commands/root.go b/cmd/kraken/commands/root.go deleted file mode 100644 index 5fc73bd..0000000 --- a/cmd/kraken/commands/root.go +++ /dev/null @@ -1,14 +0,0 @@ -package commands - -import "github.com/spf13/cobra" - -func CreateKrakenCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "kraken", - // Run: func(cmd *cobra.Command, args []string) { }, - } - - cmd.AddCommand(CreateKrakenProcessCmd()) - - return cmd -} diff --git a/cmd/kraken/kraken.go b/cmd/kraken/kraken.go deleted file mode 100644 index 0da4212..0000000 --- a/cmd/kraken/kraken.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "os" - - "git.front.kjuulh.io/kjuulh/kraken/cmd/kraken/commands" -) - -func main() { - Execute() -} - -func Execute() { - err := commands.CreateKrakenCmd().Execute() - if err != nil { - os.Exit(1) - } -} diff --git a/cmd/octopush/commands/process.go b/cmd/octopush/commands/process.go new file mode 100644 index 0000000..dcfa8ba --- /dev/null +++ b/cmd/octopush/commands/process.go @@ -0,0 +1,61 @@ +package commands + +import ( + "context" + "time" + + "git.front.kjuulh.io/kjuulh/octopush/internal/cli" + "git.front.kjuulh.io/kjuulh/octopush/internal/commands" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +func CreateOctopushProcessCmd(logger *zap.Logger) *cobra.Command { + + var ( + actionsRepo string + branch string + path string + ) + cmd := &cobra.Command{ + Use: "process", + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmd.ParseFlags(args); err != nil { + return err + } + + ctx := cmd.Context() + + deps, cleanupFunc, err := cli.Start(ctx, logger) + if err != nil { + return err + } + + defer func() { + ctx, _ = context.WithTimeout(ctx, time.Second*5) + if err := cleanupFunc(ctx); err != nil { + panic(err) + } + }() + + err = commands. + NewProcessRepos(logger, deps). + Process(ctx, actionsRepo, branch, path) + if err != nil { + return err + } + + return nil + }, + } + + pf := cmd.PersistentFlags() + + pf.StringVar(&actionsRepo, "actions-repo", "", "actions repo is the location of your actions, not where to apply the actions themselves, that should be self contained") + cmd.MarkPersistentFlagRequired("actions-repo") + pf.StringVar(&branch, "branch", "main", "which branch to look for actions in, will default to main") + pf.StringVar(&path, "path", "", "the location of the path inside the repository") + cmd.MarkPersistentFlagRequired("path") + + return cmd +} diff --git a/cmd/octopush/commands/root.go b/cmd/octopush/commands/root.go new file mode 100644 index 0000000..3a6654f --- /dev/null +++ b/cmd/octopush/commands/root.go @@ -0,0 +1,18 @@ +package commands + +import ( + "git.front.kjuulh.io/kjuulh/octopush/cmd/octopush/commands/server" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +func CreateOctopushCmd(logger *zap.Logger) *cobra.Command { + cmd := &cobra.Command{ + Use: "octopush", + } + + cmd.AddCommand(CreateOctopushProcessCmd(logger)) + cmd.AddCommand(server.CreateOctopushServerCmd(logger)) + + return cmd +} diff --git a/cmd/kraken/commands/process.go b/cmd/octopush/commands/server/process.go similarity index 95% rename from cmd/kraken/commands/process.go rename to cmd/octopush/commands/server/process.go index 89434a9..04f2041 100644 --- a/cmd/kraken/commands/process.go +++ b/cmd/octopush/commands/server/process.go @@ -1,4 +1,4 @@ -package commands +package server import ( "bytes" @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" ) -func CreateKrakenProcessCmd() *cobra.Command { +func CreateOctopushProcessCmd() *cobra.Command { var ( actionsRepo string diff --git a/cmd/octopush/commands/server/server.go b/cmd/octopush/commands/server/server.go new file mode 100644 index 0000000..90287a6 --- /dev/null +++ b/cmd/octopush/commands/server/server.go @@ -0,0 +1,16 @@ +package server + +import ( + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +func CreateOctopushServerCmd(logger *zap.Logger) *cobra.Command { + cmd := &cobra.Command{ + Use: "server", + } + + cmd.AddCommand(CreateOctopushProcessCmd()) + + return cmd +} diff --git a/cmd/octopush/octopush.go b/cmd/octopush/octopush.go new file mode 100644 index 0000000..f75223c --- /dev/null +++ b/cmd/octopush/octopush.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + + "git.front.kjuulh.io/kjuulh/octopush/cmd/octopush/commands" + "git.front.kjuulh.io/kjuulh/octopush/internal/logger" + "go.uber.org/zap" +) + +func main() { + logger, err := logger.New() + if err != nil { + panic(err) + } + _ = logger.Sync() + + zap.ReplaceGlobals(logger) + + Execute(logger) +} + +func Execute(logger *zap.Logger) { + err := commands.CreateOctopushCmd(logger).Execute() + if err != nil { + os.Exit(1) + } +} diff --git a/cmd/server/commands/root.go b/cmd/server/commands/root.go index 19c0575..379f24e 100644 --- a/cmd/server/commands/root.go +++ b/cmd/server/commands/root.go @@ -7,7 +7,7 @@ import ( func CreateServerCmd(logger *zap.Logger) *cobra.Command { cmd := &cobra.Command{ - Use: "krakenserver", + Use: "octopushserver", } cmd.AddCommand(NewStartServerCommand(logger)) diff --git a/cmd/server/commands/start.go b/cmd/server/commands/start.go index 468f133..7849f57 100644 --- a/cmd/server/commands/start.go +++ b/cmd/server/commands/start.go @@ -1,7 +1,7 @@ package commands import ( - "git.front.kjuulh.io/kjuulh/kraken/internal/server" + "git.front.kjuulh.io/kjuulh/octopush/internal/server" "github.com/spf13/cobra" "go.uber.org/zap" ) @@ -9,7 +9,7 @@ import ( func NewStartServerCommand(logger *zap.Logger) *cobra.Command { cmd := &cobra.Command{ Use: "start", - Short: "Start the kraken server", + Short: "Start the octopush server", RunE: func(cmd *cobra.Command, args []string) error { return server.Start(logger) }, diff --git a/cmd/server/server.go b/cmd/server/server.go index 5c835ec..f050505 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -3,8 +3,8 @@ package main import ( "os" - "git.front.kjuulh.io/kjuulh/kraken/cmd/server/commands" - "git.front.kjuulh.io/kjuulh/kraken/internal/logger" + "git.front.kjuulh.io/kjuulh/octopush/cmd/server/commands" + "git.front.kjuulh.io/kjuulh/octopush/internal/logger" "go.uber.org/zap" ) diff --git a/cuddle.yaml b/cuddle.yaml index 87fafac..0bd3198 100644 --- a/cuddle.yaml +++ b/cuddle.yaml @@ -3,7 +3,7 @@ base: "git@git.front.kjuulh.io:kjuulh/cuddle-go-plan.git" vars: - service: "kraken" + service: "octopush" deployments: "git@git.front.kjuulh.io:kjuulh/deployments.git" scripts: diff --git a/go.mod b/go.mod index 2fab6ad..42aca25 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,25 @@ -module git.front.kjuulh.io/kjuulh/kraken +module git.front.kjuulh.io/kjuulh/octopush go 1.19 require ( - git.front.kjuulh.io/kjuulh/curre v1.2.2 + code.gitea.io/sdk/gitea v0.15.1 + git.front.kjuulh.io/kjuulh/curre v1.3.5 github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe - github.com/ProtonMail/gopenpgp/v2 v2.4.10 github.com/gin-contrib/zap v0.0.2 github.com/gin-gonic/gin v1.8.1 github.com/go-git/go-git/v5 v5.4.2 github.com/google/uuid v1.3.0 github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.0 + github.com/whilp/git-urls v1.0.0 go.uber.org/zap v1.23.0 golang.org/x/net v0.0.0-20220909164309-bea034e7d591 + gopkg.in/yaml.v3 v3.0.1 ) require ( - code.gitea.io/sdk/gitea v0.15.1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect - github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f // indirect github.com/acomagu/bufpipe v1.0.3 // indirect github.com/cloudflare/circl v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -43,13 +43,10 @@ require ( github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect - github.com/sirupsen/logrus v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/ugorji/go/codec v1.2.7 // indirect - github.com/whilp/git-urls v1.0.0 // indirect github.com/xanzy/ssh-agent v0.3.2 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect @@ -59,5 +56,4 @@ require ( google.golang.org/protobuf v1.28.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5f09503..d326efc 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,8 @@ code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M= code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= -git.front.kjuulh.io/kjuulh/curre v1.2.2 h1:0OwWIfekrMykdQg9bdmG80I+Mjc2k4i+sy903phuDWs= -git.front.kjuulh.io/kjuulh/curre v1.2.2/go.mod h1:m7WpSehONLqPh/XF3F0BI0UOpLOfGuDmDEFI1XsM6fE= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +git.front.kjuulh.io/kjuulh/curre v1.3.5 h1:oKYh5Z0vInjViLnS4ppzK0G2Mnj7vXq8mA5i/rsWId4= +git.front.kjuulh.io/kjuulh/curre v1.3.5/go.mod h1:m7WpSehONLqPh/XF3F0BI0UOpLOfGuDmDEFI1XsM6fE= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= @@ -11,10 +10,6 @@ github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe h1:R2HeCk7SG/XpoYZlEeI1v7sId7w2AMWwzOaVqXn45FE= github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= -github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f h1:CGq7OieOz3wyQJ1fO8S0eO9TCW1JyvLrf8fhzz1i8ko= -github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= -github.com/ProtonMail/gopenpgp/v2 v2.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI= -github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= @@ -130,8 +125,6 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= @@ -173,7 +166,6 @@ go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= @@ -183,15 +175,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -210,7 +194,6 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -241,9 +224,7 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/integration_test/main_test.go b/integration_test/main_test.go index df419b6..d6132b5 100644 --- a/integration_test/main_test.go +++ b/integration_test/main_test.go @@ -7,7 +7,7 @@ import ( "os" "testing" - "git.front.kjuulh.io/kjuulh/kraken/internal/server" + "git.front.kjuulh.io/kjuulh/octopush/internal/server" "go.uber.org/zap" ) diff --git a/integration_test/storage_test.go b/integration_test/storage_test.go index 40a7956..b77e6b1 100644 --- a/integration_test/storage_test.go +++ b/integration_test/storage_test.go @@ -9,7 +9,7 @@ import ( "path" "testing" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/storage" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/storage" "github.com/stretchr/testify/require" ) diff --git a/internal/actions/action.go b/internal/actions/action.go index b36155d..07b4245 100644 --- a/internal/actions/action.go +++ b/internal/actions/action.go @@ -4,15 +4,15 @@ import ( "context" "errors" - "git.front.kjuulh.io/kjuulh/kraken/internal/actions/builders" - "git.front.kjuulh.io/kjuulh/kraken/internal/actions/querier" - "git.front.kjuulh.io/kjuulh/kraken/internal/schema" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/storage" + "git.front.kjuulh.io/kjuulh/octopush/internal/actions/builders" + "git.front.kjuulh.io/kjuulh/octopush/internal/actions/querier" + "git.front.kjuulh.io/kjuulh/octopush/internal/schema" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/storage" "go.uber.org/zap" ) type Action struct { - Schema *schema.KrakenSchema + Schema *schema.OctopushSchema SchemaPath string } diff --git a/internal/actions/action_creator.go b/internal/actions/action_creator.go index b731519..7fe4ef0 100644 --- a/internal/actions/action_creator.go +++ b/internal/actions/action_creator.go @@ -7,9 +7,9 @@ import ( "path" "time" - "git.front.kjuulh.io/kjuulh/kraken/internal/schema" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/providers" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/storage" + "git.front.kjuulh.io/kjuulh/octopush/internal/schema" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/providers" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/storage" "go.uber.org/zap" ) @@ -23,12 +23,12 @@ type ( ActionCreator struct { logger *zap.Logger storage *storage.Service - git *providers.Git + git *providers.GoGit } ActionCreatorDeps interface { GetStorageService() *storage.Service - GetGitProvider() *providers.Git + GetGitProvider() *providers.GoGit } ) @@ -59,19 +59,19 @@ func (ac *ActionCreator) Prepare(ctx context.Context, ops *ActionCreatorOps) (*A return nil, fmt.Errorf("path is invalid: %s", ops.Path) } - contents, err := os.ReadFile(path.Join(executorUrl, "kraken.yml")) + contents, err := os.ReadFile(path.Join(executorUrl, "octopush.yml")) if err != nil { return nil, err } - krakenSchema, err := schema.Unmarshal(string(contents)) + octopushSchema, err := schema.Unmarshal(string(contents)) if err != nil { return nil, err } ac.logger.Debug("Action creator done") return &Action{ - Schema: krakenSchema, + Schema: octopushSchema, SchemaPath: executorUrl, }, nil } diff --git a/internal/actions/builders/docker.go b/internal/actions/builders/docker.go index 4120229..ae11be7 100644 --- a/internal/actions/builders/docker.go +++ b/internal/actions/builders/docker.go @@ -36,7 +36,7 @@ func (g *DockerBuild) Build(ctx context.Context, modulePath, entryPath string) ( return nil, err } tag := hex.EncodeToString(b) - buildDockerCmd := fmt.Sprintf("(cd %s; docker build -f %s --tag kraken/%s .)", modulePath, entryPath, tag) + buildDockerCmd := fmt.Sprintf("(cd %s; docker build -f %s --tag octopush/%s .)", modulePath, entryPath, tag) g.logger.Debug("Running command", zap.String("command", buildDockerCmd)) cmd := exec.CommandContext( @@ -73,7 +73,7 @@ func (g *DockerBuild) Build(ctx context.Context, modulePath, entryPath string) ( ctx, "/bin/bash", "-c", - fmt.Sprintf("docker run --rm -v %s/:/src/work/ kraken/%s", victimPath, tag), + fmt.Sprintf("docker run --rm -v %s/:/src/work/ octopush/%s", victimPath, tag), ) runDockerWriter := &zapio.Writer{ diff --git a/internal/api/process_command.go b/internal/api/process_command.go index 2e852b5..b067615 100644 --- a/internal/api/process_command.go +++ b/internal/api/process_command.go @@ -4,9 +4,9 @@ import ( "context" "net/http" - "git.front.kjuulh.io/kjuulh/kraken/internal/commands" - "git.front.kjuulh.io/kjuulh/kraken/internal/serverdeps" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/jobs" + "git.front.kjuulh.io/kjuulh/octopush/internal/commands" + "git.front.kjuulh.io/kjuulh/octopush/internal/serverdeps" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/jobs" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" diff --git a/internal/api/root.go b/internal/api/root.go index 7f814ac..c4f36a4 100644 --- a/internal/api/root.go +++ b/internal/api/root.go @@ -1,7 +1,7 @@ package api import ( - "git.front.kjuulh.io/kjuulh/kraken/internal/serverdeps" + "git.front.kjuulh.io/kjuulh/octopush/internal/serverdeps" "github.com/gin-gonic/gin" "go.uber.org/zap" ) diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..d128ad9 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,30 @@ +package cli + +import ( + "context" + + "git.front.kjuulh.io/kjuulh/curre" + "git.front.kjuulh.io/kjuulh/octopush/internal/server" + "git.front.kjuulh.io/kjuulh/octopush/internal/serverdeps" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/signer" + "go.uber.org/zap" +) + +func Start(ctx context.Context, logger *zap.Logger) (*serverdeps.ServerDeps, curre.CleanupFunc, error) { + deps := serverdeps.NewServerDeps(logger) + + readyChan := make(chan curre.ComponentsAreReady, 1) + + cleanupFunc, err := curre.NewManager(). + Register( + server.NewStorageServer(logger.With(zap.Namespace("storage")), deps), + ). + Register( + signer.NewOpenPGPApp(deps.GetOpenPGP()), + ). + RunNonBlocking(ctx, readyChan) + + <-readyChan + + return deps, cleanupFunc, err +} diff --git a/internal/commands/process_repos.go b/internal/commands/process_repos.go index 02c7c27..316298e 100644 --- a/internal/commands/process_repos.go +++ b/internal/commands/process_repos.go @@ -7,11 +7,11 @@ import ( "sync" "time" - "git.front.kjuulh.io/kjuulh/kraken/internal/actions" - "git.front.kjuulh.io/kjuulh/kraken/internal/gitproviders" - "git.front.kjuulh.io/kjuulh/kraken/internal/schema" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/providers" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/storage" + "git.front.kjuulh.io/kjuulh/octopush/internal/actions" + "git.front.kjuulh.io/kjuulh/octopush/internal/gitproviders" + "git.front.kjuulh.io/kjuulh/octopush/internal/schema" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/providers" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/storage" giturls "github.com/whilp/git-urls" "go.uber.org/zap" ) @@ -20,14 +20,14 @@ type ( ProcessRepos struct { logger *zap.Logger storage *storage.Service - git *providers.Git + git *providers.GoGit actionCreator *actions.ActionCreator gitea *gitproviders.Gitea } ProcessReposDeps interface { GetStorageService() *storage.Service - GetGitProvider() *providers.Git + GetGitProvider() *providers.GoGit GetActionCreator() *actions.ActionCreator GetGitea() *gitproviders.Gitea } @@ -79,7 +79,7 @@ func (pr *ProcessRepos) Process(ctx context.Context, repository string, branch s return nil } -func (pr *ProcessRepos) getRepoUrls(ctx context.Context, schema *schema.KrakenSchema) ([]string, error) { +func (pr *ProcessRepos) getRepoUrls(ctx context.Context, schema *schema.OctopushSchema) ([]string, error) { repoUrls := make([]string, 0) repoUrls = append(repoUrls, schema.Select.Repositories...) @@ -161,7 +161,7 @@ func (pr *ProcessRepos) prepareAction( return cleanupfunc, area, nil } -func (pr *ProcessRepos) clone(ctx context.Context, area *storage.Area, repoUrl string) (*providers.GitRepo, error) { +func (pr *ProcessRepos) clone(ctx context.Context, area *storage.Area, repoUrl string) (*providers.GoGitRepo, error) { pr.logger.Debug("Cloning repo", zap.String("path", area.Path), zap.String("repoUrl", repoUrl)) cloneCtx, _ := context.WithTimeout(ctx, time.Second*5) repo, err := pr.git.Clone(cloneCtx, area, repoUrl) @@ -177,7 +177,7 @@ func (pr *ProcessRepos) clone(ctx context.Context, area *storage.Area, repoUrl s return repo, nil } -func (pr *ProcessRepos) commit(ctx context.Context, area *storage.Area, repo *providers.GitRepo, repoUrl string) error { +func (pr *ProcessRepos) commit(ctx context.Context, area *storage.Area, repo *providers.GoGitRepo, repoUrl string) error { wt, err := pr.git.Add(ctx, area, repo) if err != nil { return fmt.Errorf("could not add file: %w", err) @@ -189,8 +189,9 @@ func (pr *ProcessRepos) commit(ctx context.Context, area *storage.Area, repo *pr } if status.IsClean() { + // TODO: check for pr pr.logger.Info("Returning early, as no modifications are detected") - return nil + //return nil } err = pr.git.Commit(ctx, repo) @@ -231,7 +232,7 @@ func (pr *ProcessRepos) commit(ctx context.Context, area *storage.Area, repo *pr return err } - err = pr.gitea.CreatePr(ctx, fmt.Sprintf("%s://%s", "https", url.Host), org, semanticName, head, originHead, "kraken-apply") + err = pr.gitea.CreatePr(ctx, fmt.Sprintf("%s://%s", "https", url.Host), org, semanticName, head, originHead, "octopush-apply") if err != nil { return err } diff --git a/internal/logger/zap.go b/internal/logger/zap.go index b6e7dce..b3b10c9 100644 --- a/internal/logger/zap.go +++ b/internal/logger/zap.go @@ -12,7 +12,7 @@ func New() (*zap.Logger, error) { return lvl >= zapcore.ErrorLevel }) lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { - return lvl < zapcore.ErrorLevel + return lvl < zapcore.ErrorLevel // && lvl > zapcore.DebugLevel }) config := zap.NewDevelopmentEncoderConfig() @@ -28,5 +28,6 @@ func New() (*zap.Logger, error) { ) logger := zap.New(core) + return logger, nil } diff --git a/internal/schema/kraken.go b/internal/schema/kraken.go index b1c76f5..afa6f2d 100644 --- a/internal/schema/kraken.go +++ b/internal/schema/kraken.go @@ -2,7 +2,7 @@ package schema import "gopkg.in/yaml.v3" -type KrakenSchema struct { +type OctopushSchema struct { ApiVersion string `yaml:"apiVersion"` Name string `yaml:"name"` Select struct { @@ -22,8 +22,8 @@ type KrakenSchema struct { } `yaml:"queries"` } -func Unmarshal(raw string) (*KrakenSchema, error) { - k := &KrakenSchema{} +func Unmarshal(raw string) (*OctopushSchema, error) { + k := &OctopushSchema{} err := yaml.Unmarshal([]byte(raw), k) if err != nil { return nil, err diff --git a/internal/server/http_server.go b/internal/server/http_server.go index 3daa290..b367049 100644 --- a/internal/server/http_server.go +++ b/internal/server/http_server.go @@ -7,8 +7,8 @@ import ( "time" "git.front.kjuulh.io/kjuulh/curre" - "git.front.kjuulh.io/kjuulh/kraken/internal/api" - "git.front.kjuulh.io/kjuulh/kraken/internal/serverdeps" + "git.front.kjuulh.io/kjuulh/octopush/internal/api" + "git.front.kjuulh.io/kjuulh/octopush/internal/serverdeps" ginzap "github.com/gin-contrib/zap" "github.com/gin-gonic/gin" "go.uber.org/zap" diff --git a/internal/server/server.go b/internal/server/server.go index a71cbd7..cc946eb 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,8 +4,8 @@ import ( "context" "git.front.kjuulh.io/kjuulh/curre" - "git.front.kjuulh.io/kjuulh/kraken/internal/serverdeps" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/signer" + "git.front.kjuulh.io/kjuulh/octopush/internal/serverdeps" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/signer" "go.uber.org/zap" ) diff --git a/internal/server/storage_server.go b/internal/server/storage_server.go index 5be0a36..9a18493 100644 --- a/internal/server/storage_server.go +++ b/internal/server/storage_server.go @@ -5,7 +5,7 @@ import ( "time" "git.front.kjuulh.io/kjuulh/curre" - "git.front.kjuulh.io/kjuulh/kraken/internal/serverdeps" + "git.front.kjuulh.io/kjuulh/octopush/internal/serverdeps" "go.uber.org/zap" ) diff --git a/internal/serverdeps/server_deps.go b/internal/serverdeps/server_deps.go index da9c333..f52a88c 100644 --- a/internal/serverdeps/server_deps.go +++ b/internal/serverdeps/server_deps.go @@ -1,12 +1,12 @@ package serverdeps import ( - actionc "git.front.kjuulh.io/kjuulh/kraken/internal/actions" - "git.front.kjuulh.io/kjuulh/kraken/internal/gitproviders" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/actions" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/providers" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/signer" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/storage" + actionc "git.front.kjuulh.io/kjuulh/octopush/internal/actions" + "git.front.kjuulh.io/kjuulh/octopush/internal/gitproviders" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/actions" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/providers" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/signer" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/storage" "go.uber.org/zap" ) @@ -53,7 +53,7 @@ func (deps *ServerDeps) GetStorageService() *storage.Service { return storage.NewService(deps.logger.With(zap.Namespace("storage")), deps.storageConfig) } -func (deps *ServerDeps) GetGitProvider() *providers.Git { +func (deps *ServerDeps) GetGitProvider() *providers.GoGit { return providers.NewGit(deps.logger.With(zap.Namespace("gitProvider")), deps.gitCfg, deps.openPGP) } diff --git a/internal/services/actions/action.go b/internal/services/actions/action.go index 5262569..c711b47 100644 --- a/internal/services/actions/action.go +++ b/internal/services/actions/action.go @@ -3,7 +3,7 @@ package actions import ( "context" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/storage" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/storage" "go.uber.org/zap" ) diff --git a/internal/services/providers/git.go b/internal/services/providers/git.go index 32cc1a7..cf2a3ba 100644 --- a/internal/services/providers/git.go +++ b/internal/services/providers/git.go @@ -1,339 +1 @@ package providers - -import ( - "context" - "errors" - "fmt" - "time" - - "git.front.kjuulh.io/kjuulh/kraken/internal/services/signer" - "git.front.kjuulh.io/kjuulh/kraken/internal/services/storage" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/plumbing/transport/ssh" - "go.uber.org/zap" - "go.uber.org/zap/zapio" -) - -// Git is a native git provider, it can clone, pull -// , push and as in abstraction on native git operations -type Git struct { - logger *zap.Logger - gitConfig *GitConfig - openPGP *signer.OpenPGP -} - -type GitRepo struct { - repo *git.Repository -} - -func (gr *GitRepo) GetHEAD() (string, error) { - head, err := gr.repo.Head() - if err != nil { - return "", err - } - - return head.Name().Short(), nil -} - -type GitAuth string - -const ( - GIT_AUTH_SSH GitAuth = "ssh" - GIT_AUTH_USERNAME_PASSWORD GitAuth = "username_password" - GIT_AUTH_ACCESS_TOKEN GitAuth = "access_token" - GIT_AUTH_ANONYMOUS GitAuth = "anonymous" - GIT_AUTH_SSH_AGENT GitAuth = "ssh_agent" -) - -type GitConfig struct { - AuthOption GitAuth - User string - Password string - AccessToken string - SshPublicKeyFilePath string - SshPrivateKeyPassword string -} - -func NewGit(logger *zap.Logger, gitConfig *GitConfig, openPGP *signer.OpenPGP) *Git { - return &Git{logger: logger, gitConfig: gitConfig, openPGP: openPGP} -} - -func (g *Git) GetOriginHEADForRepo(ctx context.Context, gitRepo *GitRepo) (string, error) { - auth, err := g.GetAuth() - if err != nil { - return "", err - } - - remote, err := gitRepo.repo.Remote("origin") - if err != nil { - return "", err - } - - refs, err := remote.ListContext(ctx, &git.ListOptions{ - Auth: auth, - }) - if err != nil { - return "", err - } - - headRef := "" - for _, ref := range refs { - //g.logger.Debug(ref.String()) - if !ref.Name().IsBranch() { - headRef = ref.Target().Short() - } - } - - if headRef == "" { - return "", errors.New("no upstream HEAD branch could be found") - } - - return headRef, nil -} - -func (g *Git) CloneBranch(ctx context.Context, storageArea *storage.Area, repoUrl string, branch string) (*GitRepo, error) { - g.logger.Debug( - "cloning repository", - zap.String("repoUrl", repoUrl), - zap.String("path", storageArea.Path), - ) - - auth, err := g.GetAuth() - if err != nil { - return nil, err - } - - cloneOptions := git.CloneOptions{ - URL: repoUrl, - Auth: auth, - RemoteName: "origin", - ReferenceName: plumbing.NewBranchReferenceName(branch), - SingleBranch: false, - NoCheckout: false, - Depth: 1, - RecurseSubmodules: 1, - Progress: g.getProgressWriter(), - Tags: 0, - InsecureSkipTLS: false, - CABundle: []byte{}, - } - - repo, err := git.PlainCloneContext(ctx, storageArea.Path, false, &cloneOptions) - if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { - return nil, err - } - - g.logger.Debug("done cloning repo") - - return &GitRepo{repo: repo}, nil -} - -func (g *Git) Clone(ctx context.Context, storageArea *storage.Area, repoUrl string) (*GitRepo, error) { - g.logger.Debug( - "cloning repository", - zap.String("repoUrl", repoUrl), - zap.String("path", storageArea.Path), - ) - - auth, err := g.GetAuth() - if err != nil { - return nil, err - } - - cloneOptions := git.CloneOptions{ - URL: repoUrl, - Auth: auth, - RemoteName: "origin", - ReferenceName: "refs/heads/main", - SingleBranch: false, - NoCheckout: false, - Depth: 1, - RecurseSubmodules: 1, - Progress: g.getProgressWriter(), - Tags: 0, - InsecureSkipTLS: false, - CABundle: []byte{}, - } - - repo, err := git.PlainCloneContext(ctx, storageArea.Path, false, &cloneOptions) - if err != nil { - return nil, err - } - - g.logger.Debug("done cloning repo") - - return &GitRepo{repo: repo}, nil -} - -func (g *Git) getProgressWriter() *zapio.Writer { - return &zapio.Writer{ - Log: g.logger.With(zap.String("process", "go-git")), - Level: zap.DebugLevel, - } -} - -func (g *Git) Add(ctx context.Context, storageArea *storage.Area, gitRepo *GitRepo) (*git.Worktree, error) { - worktree, err := gitRepo.repo.Worktree() - if err != nil { - return nil, err - } - - err = worktree.AddWithOptions(&git.AddOptions{ - All: true, - }) - if err != nil { - return nil, err - } - - status, err := worktree.Status() - if err != nil { - return nil, err - } - - g.logger.Debug("git status", zap.String("status", status.String())) - - return worktree, nil -} - -func (g *Git) CreateBranch(ctx context.Context, gitRepo *GitRepo) error { - worktree, err := gitRepo.repo.Worktree() - if err != nil { - return err - } - - refSpec := plumbing.NewBranchReferenceName("kraken-apply") - err = gitRepo.repo.CreateBranch(&config.Branch{ - Name: "kraken-apply", - Remote: "origin", - Merge: refSpec, - Rebase: "", - }) - if err != nil { - return fmt.Errorf("could not create branch: %w", err) - } - - err = worktree.Checkout(&git.CheckoutOptions{ - Branch: plumbing.ReferenceName(refSpec.String()), - Create: true, - Force: false, - Keep: false, - }) - if err != nil { - return fmt.Errorf("could not checkout branch: %w", err) - } - - remoteRef := plumbing.NewRemoteReferenceName("origin", "kraken-apply") - ref := plumbing.NewSymbolicReference(refSpec, remoteRef) - err = gitRepo.repo.Storer.SetReference(ref) - if err != nil { - return fmt.Errorf("could not set reference: %w", err) - } - - auth, err := g.GetAuth() - if err != nil { - return err - } - - err = worktree.PullContext(ctx, &git.PullOptions{ - RemoteName: "origin", - ReferenceName: "refs/heads/main", - SingleBranch: false, - Depth: 1, - Auth: auth, - RecurseSubmodules: 1, - Progress: g.getProgressWriter(), - Force: true, - InsecureSkipTLS: false, - CABundle: []byte{}, - }) - if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { - return fmt.Errorf("could not pull from origin: %w", err) - } - - g.logger.Debug("done creating branches") - - return nil -} - -func (g *Git) Commit(ctx context.Context, gitRepo *GitRepo) error { - worktree, err := gitRepo.repo.Worktree() - if err != nil { - return err - } - - _, err = worktree.Commit("some-commit", &git.CommitOptions{ - All: true, - Author: &object.Signature{Name: "kraken", Email: "kraken@kasperhermansen.com", When: time.Now()}, - Committer: &object.Signature{Name: "kraken", Email: "kraken@kasperhermansen.com", When: time.Now()}, - SignKey: g.openPGP.SigningKey, - }) - if err != nil { - return err - } - - g.logger.Debug("done commiting objects") - - return nil -} - -func (g *Git) Push(ctx context.Context, gitRepo *GitRepo) error { - auth, err := g.GetAuth() - if err != nil { - return err - } - - err = gitRepo.repo.PushContext(ctx, &git.PushOptions{ - RemoteName: "origin", - RefSpecs: []config.RefSpec{}, - Auth: auth, - Progress: g.getProgressWriter(), - Prune: false, - Force: true, - InsecureSkipTLS: false, - CABundle: []byte{}, - RequireRemoteRefs: []config.RefSpec{}, - }) - if err != nil { - return err - } - - g.logger.Debug("done pushing branch") - - return nil -} - -func (g *Git) GetAuth() (transport.AuthMethod, error) { - switch g.gitConfig.AuthOption { - case GIT_AUTH_SSH: - sshKey, err := ssh.NewPublicKeysFromFile( - g.gitConfig.User, - g.gitConfig.SshPublicKeyFilePath, - g.gitConfig.SshPrivateKeyPassword, - ) - if err != nil { - return nil, err - } - return sshKey, nil - case GIT_AUTH_USERNAME_PASSWORD: - return &http.BasicAuth{ - Username: g.gitConfig.User, - Password: g.gitConfig.Password, - }, nil - case GIT_AUTH_ACCESS_TOKEN: - return &http.BasicAuth{ - Username: "required-username", - Password: g.gitConfig.AccessToken, - }, nil - case GIT_AUTH_ANONYMOUS: - return nil, nil - case GIT_AUTH_SSH_AGENT: - return ssh.NewSSHAgentAuth(g.gitConfig.User) - default: - return nil, nil - } -} diff --git a/internal/services/providers/gogit.go b/internal/services/providers/gogit.go new file mode 100644 index 0000000..3e1eacd --- /dev/null +++ b/internal/services/providers/gogit.go @@ -0,0 +1,339 @@ +package providers + +import ( + "context" + "errors" + "fmt" + "time" + + "git.front.kjuulh.io/kjuulh/octopush/internal/services/signer" + "git.front.kjuulh.io/kjuulh/octopush/internal/services/storage" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "go.uber.org/zap" + "go.uber.org/zap/zapio" +) + +// GoGit is a native git provider, it can clone, pull +// , push and as in abstraction on native git operations +type GoGit struct { + logger *zap.Logger + gitConfig *GitConfig + openPGP *signer.OpenPGP +} + +type GoGitRepo struct { + repo *git.Repository +} + +func (gr *GoGitRepo) GetHEAD() (string, error) { + head, err := gr.repo.Head() + if err != nil { + return "", err + } + + return head.Name().Short(), nil +} + +type GitAuth string + +const ( + GIT_AUTH_SSH GitAuth = "ssh" + GIT_AUTH_USERNAME_PASSWORD GitAuth = "username_password" + GIT_AUTH_ACCESS_TOKEN GitAuth = "access_token" + GIT_AUTH_ANONYMOUS GitAuth = "anonymous" + GIT_AUTH_SSH_AGENT GitAuth = "ssh_agent" +) + +type GitConfig struct { + AuthOption GitAuth + User string + Password string + AccessToken string + SshPublicKeyFilePath string + SshPrivateKeyPassword string +} + +func NewGit(logger *zap.Logger, gitConfig *GitConfig, openPGP *signer.OpenPGP) *GoGit { + return &GoGit{logger: logger, gitConfig: gitConfig, openPGP: openPGP} +} + +func (g *GoGit) GetOriginHEADForRepo(ctx context.Context, gitRepo *GoGitRepo) (string, error) { + auth, err := g.GetAuth() + if err != nil { + return "", err + } + + remote, err := gitRepo.repo.Remote("origin") + if err != nil { + return "", err + } + + refs, err := remote.ListContext(ctx, &git.ListOptions{ + Auth: auth, + }) + if err != nil { + return "", err + } + + headRef := "" + for _, ref := range refs { + //g.logger.Debug(ref.String()) + if ref.Target().IsBranch() { + headRef = ref.Target().Short() + } + } + + if headRef == "" { + return "", errors.New("no upstream HEAD branch could be found") + } + + return headRef, nil +} + +func (g *GoGit) CloneBranch(ctx context.Context, storageArea *storage.Area, repoUrl string, branch string) (*GoGitRepo, error) { + g.logger.Debug( + "cloning repository", + zap.String("repoUrl", repoUrl), + zap.String("path", storageArea.Path), + ) + + auth, err := g.GetAuth() + if err != nil { + return nil, err + } + + cloneOptions := git.CloneOptions{ + URL: repoUrl, + Auth: auth, + RemoteName: "origin", + ReferenceName: plumbing.NewBranchReferenceName(branch), + SingleBranch: false, + NoCheckout: false, + Depth: 1, + RecurseSubmodules: 1, + Progress: g.getProgressWriter(), + Tags: 0, + InsecureSkipTLS: false, + CABundle: []byte{}, + } + + repo, err := git.PlainCloneContext(ctx, storageArea.Path, false, &cloneOptions) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + return nil, err + } + + g.logger.Debug("done cloning repo") + + return &GoGitRepo{repo: repo}, nil +} + +func (g *GoGit) Clone(ctx context.Context, storageArea *storage.Area, repoUrl string) (*GoGitRepo, error) { + g.logger.Debug( + "cloning repository", + zap.String("repoUrl", repoUrl), + zap.String("path", storageArea.Path), + ) + + auth, err := g.GetAuth() + if err != nil { + return nil, err + } + + cloneOptions := git.CloneOptions{ + URL: repoUrl, + Auth: auth, + RemoteName: "origin", + ReferenceName: "", + SingleBranch: false, + NoCheckout: false, + Depth: 1, + RecurseSubmodules: 1, + Progress: g.getProgressWriter(), + Tags: 0, + InsecureSkipTLS: false, + CABundle: []byte{}, + } + + repo, err := git.PlainCloneContext(ctx, storageArea.Path, false, &cloneOptions) + if err != nil { + return nil, err + } + + g.logger.Debug("done cloning repo") + + return &GoGitRepo{repo: repo}, nil +} + +func (g *GoGit) getProgressWriter() *zapio.Writer { + return &zapio.Writer{ + Log: g.logger.With(zap.String("process", "go-git")), + Level: zap.DebugLevel, + } +} + +func (g *GoGit) Add(ctx context.Context, storageArea *storage.Area, gitRepo *GoGitRepo) (*git.Worktree, error) { + worktree, err := gitRepo.repo.Worktree() + if err != nil { + return nil, err + } + + err = worktree.AddWithOptions(&git.AddOptions{ + All: true, + }) + if err != nil { + return nil, err + } + + status, err := worktree.Status() + if err != nil { + return nil, err + } + + g.logger.Debug("git status", zap.String("status", status.String())) + + return worktree, nil +} + +func (g *GoGit) CreateBranch(ctx context.Context, gitRepo *GoGitRepo) error { + worktree, err := gitRepo.repo.Worktree() + if err != nil { + return err + } + + refSpec := plumbing.NewBranchReferenceName("octopush-apply") + err = gitRepo.repo.CreateBranch(&config.Branch{ + Name: "octopush-apply", + Remote: "origin", + Merge: refSpec, + Rebase: "", + }) + if err != nil { + return fmt.Errorf("could not create branch: %w", err) + } + + err = worktree.Checkout(&git.CheckoutOptions{ + Branch: plumbing.ReferenceName(refSpec.String()), + Create: true, + Force: false, + Keep: false, + }) + if err != nil { + return fmt.Errorf("could not checkout branch: %w", err) + } + + //remoteRef := plumbing.NewRemoteReferenceName("origin", "octopush-apply") + //ref := plumbing.NewSymbolicReference(refSpec, remoteRef) + //err = gitRepo.repo.Storer.SetReference(ref) + //if err != nil { + // return fmt.Errorf("could not set reference: %w", err) + //} + + auth, err := g.GetAuth() + if err != nil { + return err + } + + err = worktree.PullContext(ctx, &git.PullOptions{ + RemoteName: "origin", + ReferenceName: "", + SingleBranch: false, + Depth: 1, + Auth: auth, + RecurseSubmodules: 1, + Progress: g.getProgressWriter(), + Force: true, + InsecureSkipTLS: false, + CABundle: []byte{}, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + return fmt.Errorf("could not pull from origin: %w", err) + } + + g.logger.Debug("done creating branches") + + return nil +} + +func (g *GoGit) Commit(ctx context.Context, gitRepo *GoGitRepo) error { + worktree, err := gitRepo.repo.Worktree() + if err != nil { + return err + } + + _, err = worktree.Commit("some-commit", &git.CommitOptions{ + All: true, + Author: &object.Signature{Name: "octopush", Email: "octopush@kasperhermansen.com", When: time.Now()}, + Committer: &object.Signature{Name: "octopush", Email: "octopush@kasperhermansen.com", When: time.Now()}, + SignKey: g.openPGP.SigningKey, + }) + if err != nil { + return err + } + + g.logger.Debug("done commiting objects") + + return nil +} + +func (g *GoGit) Push(ctx context.Context, gitRepo *GoGitRepo) error { + auth, err := g.GetAuth() + if err != nil { + return err + } + + err = gitRepo.repo.PushContext(ctx, &git.PushOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{}, + Auth: auth, + Progress: g.getProgressWriter(), + Prune: false, + Force: true, + InsecureSkipTLS: false, + CABundle: []byte{}, + RequireRemoteRefs: []config.RefSpec{}, + }) + if err != nil { + return err + } + + g.logger.Debug("done pushing branch") + + return nil +} + +func (g *GoGit) GetAuth() (transport.AuthMethod, error) { + switch g.gitConfig.AuthOption { + case GIT_AUTH_SSH: + sshKey, err := ssh.NewPublicKeysFromFile( + g.gitConfig.User, + g.gitConfig.SshPublicKeyFilePath, + g.gitConfig.SshPrivateKeyPassword, + ) + if err != nil { + return nil, err + } + return sshKey, nil + case GIT_AUTH_USERNAME_PASSWORD: + return &http.BasicAuth{ + Username: g.gitConfig.User, + Password: g.gitConfig.Password, + }, nil + case GIT_AUTH_ACCESS_TOKEN: + return &http.BasicAuth{ + Username: "required-username", + Password: g.gitConfig.AccessToken, + }, nil + case GIT_AUTH_ANONYMOUS: + return nil, nil + case GIT_AUTH_SSH_AGENT: + return ssh.NewSSHAgentAuth(g.gitConfig.User) + default: + return nil, nil + } +} diff --git a/internal/services/storage/storage.go b/internal/services/storage/storage.go index 01a2625..89b8a90 100644 --- a/internal/services/storage/storage.go +++ b/internal/services/storage/storage.go @@ -22,7 +22,7 @@ func NewDefaultStorageConfig() (*StorageConfig, error) { return nil, err } return &StorageConfig{ - Path: path.Join(tempDir, "kraken"), + Path: path.Join(tempDir, "octopush"), }, nil } diff --git a/roadmap.md b/roadmap.md index 34cf540..109c79d 100644 --- a/roadmap.md +++ b/roadmap.md @@ -19,26 +19,38 @@ ## Version 0.1 - [x] Setup a way to choose actions and predicates -- [x] Allow instantiation of actions, kraken template repo etc. +- [x] Allow instantiation of actions, octopush template repo etc. - [x] Implement docker action - [x] Create pr for gitea provider - [x] Providing query results -- [ ] Create CLI to trigger action +- [x] Create CLI to trigger action -### Not in scope +## Version 0.2 -## Version 1.0 - -- [ ] Write README +- [x] Write README +- [ ] Make select depend on query - [ ] Make configurable ssh user - [ ] Make configurable gpg keyset - [ ] Make configurable git provider -- [ ] Create templating function + - [ ] Add github +- [ ] Create templating function for easily creating new actions - [ ] Add way to see progress of runners -- [ ] Implement global .kraken store for easy access -- [ ] Move builders to start instead of every time +- [ ] Implement global .octopush store for easy access to settings +- [ ] Move builders to start instead of every building on every action +- [ ] Setup releases on github +- [ ] Setup CI +- [ ] Setup static analysis +- [ ] Setup releases on gitea using drone +- [ ] Figure out a license (probably MIT) -## Version 1.x +## Version 0.3 + +- [ ] Create setup version for local actions +- [ ] Create setup version for server actions +- [ ] Create json schema +- [ ] Move roadmap to release / changelog + +## Version 0.x - Think about some sort of isolation - Run authenticated on servers diff --git a/scripts/push_github.sh b/scripts/push_github.sh index d2a19cb..f8032db 100755 --- a/scripts/push_github.sh +++ b/scripts/push_github.sh @@ -2,6 +2,6 @@ set -e -git remote add github git@github.com:kjuulh/kraken.git || true +git remote add github git@github.com:kjuulh/octopush.git || true git push -f github main diff --git a/scripts/run_client.sh b/scripts/run_client.sh index 237c23d..8ab48f4 100755 --- a/scripts/run_client.sh +++ b/scripts/run_client.sh @@ -4,5 +4,10 @@ set -e current_branch=$(git branch --show-current) -go run cmd/kraken/kraken.go process --actions-repo "git@git.front.kjuulh.io:kjuulh/kraken.git" --branch "$current_branch" --path "_examples/actions/write_a_readme" -go run cmd/kraken/kraken.go process --actions-repo "git@git.front.kjuulh.io:kjuulh/kraken.git" --branch "$current_branch" --path "_examples/queries/scrape_readme" +export $(cat .env | xargs) + +#go run cmd/octopush/octopush.go process --actions-repo "git@git.front.kjuulh.io:kjuulh/octopush.git" --branch "$current_branch" --path "_examples/actions/write_a_readme" +go run cmd/octopush/octopush.go process \ + --actions-repo "git@git.front.kjuulh.io:kjuulh/octopush.git"\ + --branch "$current_branch" \ + --path "_examples/actions/add_releaserc" -- 2.45.2 From 7a915ff9afdb0cfea6a66c9aed18212599bfd8ca Mon Sep 17 00:00:00 2001 From: kjuulh Date: Wed, 21 Sep 2022 22:55:31 +0200 Subject: [PATCH 2/2] Push roadmap --- roadmap.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/roadmap.md b/roadmap.md index 109c79d..2e91660 100644 --- a/roadmap.md +++ b/roadmap.md @@ -28,6 +28,11 @@ ## Version 0.2 - [x] Write README +- [x] Fix git issues +- [x] Allow octopush to run directly on the cli + +## Version 0.3 + - [ ] Make select depend on query - [ ] Make configurable ssh user - [ ] Make configurable gpg keyset @@ -43,7 +48,7 @@ - [ ] Setup releases on gitea using drone - [ ] Figure out a license (probably MIT) -## Version 0.3 +## Version 0.4 - [ ] Create setup version for local actions - [ ] Create setup version for server actions -- 2.45.2