From 355b706d08915d51a1ea071f06a7b6798d4ea21a Mon Sep 17 00:00:00 2001 From: Cosmic-Skye <7584038+thecosmicskye@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:49:45 -0400 Subject: [PATCH 1/2] Add GitLab CE integration --- gitlab/Makefile | 33 ++++ gitlab/README.md | 76 ++++++++ gitlab/config.go | 375 ++++++++++++++++++++++++++++++++++++ gitlab/config_test.go | 91 +++++++++ gitlab/exporter.go | 88 +++++++++ gitlab/exporter/main.go | 12 ++ gitlab/exporter/schema.json | 61 ++++++ gitlab/go.mod | 39 ++++ gitlab/go.sum | 187 ++++++++++++++++++ gitlab/importer.go | 100 ++++++++++ gitlab/importer/main.go | 12 ++ gitlab/importer/schema.json | 62 ++++++ gitlab/manifest.yaml | 21 ++ 13 files changed, 1157 insertions(+) create mode 100644 gitlab/Makefile create mode 100644 gitlab/README.md create mode 100644 gitlab/config.go create mode 100644 gitlab/config_test.go create mode 100644 gitlab/exporter.go create mode 100644 gitlab/exporter/main.go create mode 100644 gitlab/exporter/schema.json create mode 100644 gitlab/go.mod create mode 100644 gitlab/go.sum create mode 100644 gitlab/importer.go create mode 100644 gitlab/importer/main.go create mode 100644 gitlab/importer/schema.json create mode 100644 gitlab/manifest.yaml diff --git a/gitlab/Makefile b/gitlab/Makefile new file mode 100644 index 0000000..5052269 --- /dev/null +++ b/gitlab/Makefile @@ -0,0 +1,33 @@ +GO = go +EXT = + +PLAKAR ?= plakar +VERSION ?= v1.0.0 + +GOOS := $(shell go env GOOS) +GOARCH := $(shell go env GOARCH) +PTAR := gitlab_$(VERSION)_$(GOOS)_$(GOARCH).ptar + +all: build + +build: + ${GO} build -v -o gitlabImporter${EXT} ./importer + ${GO} build -v -o gitlabExporter${EXT} ./exporter + +package: build + rm -f $(PTAR) + $(PLAKAR) pkg create ./manifest.yaml $(VERSION) + +uninstall: + -$(PLAKAR) pkg rm gitlab + +install: package + $(PLAKAR) pkg add ./$(PTAR) + +reinstall: uninstall install + +test: + ${GO} test ./... + +clean: + rm -f gitlabImporter gitlabExporter gitlab_*.ptar diff --git a/gitlab/README.md b/gitlab/README.md new file mode 100644 index 0000000..abdbc93 --- /dev/null +++ b/gitlab/README.md @@ -0,0 +1,76 @@ +# GitLab CE Integration for Plakar + +Backup and restore a self-hosted GitLab CE instance with GitLab's native +`gitlab-backup` tooling. + +The importer invokes `gitlab-backup create`, ingests the newest generated +`*_gitlab_backup.tar` archive, and includes GitLab configuration files. The +exporter writes those files to a target instance and invokes +`gitlab-backup restore`. + +## Prerequisites + +- Plakar >= 1.1 +- GitLab CE with `gitlab-backup` available on the source and restore target +- Permission to read the GitLab backup directory and config files +- For remote operation: SSH access to the GitLab host + +Use `use_sudo=true` when the account needs non-interactive sudo privileges for +GitLab backup commands or protected config paths. + +## Configuration + +| Option | Default | Description | +| --- | --- | --- | +| `location` | `gitlab://local` | GitLab source or target URI. | +| `backup_path` | `/var/opt/gitlab/backups` | Directory containing GitLab backup archives. | +| `config_paths` | `/etc/gitlab/gitlab.rb,/etc/gitlab/gitlab-secrets.json` | Comma-separated config files to include during backup. | +| `config_dir` | `/etc/gitlab` | Config restore directory. | +| `gitlab_backup_bin` | `gitlab-backup` | Backup CLI binary name or path. | +| `use_sudo` | `false` | Prefix privileged operations with `sudo -n`. | +| `ssh_host` | unset | Remote host. When set, the integration runs through SSH. | +| `ssh_user` | unset | SSH username. | +| `ssh_port` | unset | SSH port. | +| `ssh_identity_file` | unset | SSH private key path. | +| `ssh_bin` | `ssh` | SSH binary name or path. | +| `force` | `false` | Exporter only: pass `force=yes` to `gitlab-backup restore`. | + +## Examples + +Back up a local GitLab instance: + +```sh +plakar backup gitlab://local use_sudo=true +``` + +Back up a remote GitLab instance over SSH: + +```sh +plakar backup gitlab://gitlab.example.com \ + ssh_host=gitlab.example.com \ + ssh_user=git \ + use_sudo=true +``` + +Restore to a local GitLab instance: + +```sh +plakar restore -to gitlab://local use_sudo=true +``` + +Restore to a remote GitLab host: + +```sh +plakar restore -to gitlab://gitlab.example.com \ + ssh_host=gitlab.example.com \ + ssh_user=git \ + use_sudo=true \ + +``` + +## Restore Notes + +GitLab restore has operational prerequisites outside Plakar, including stopping +services that write to the database before running restore and running GitLab's +post-restore checks afterward. Follow the GitLab backup and restore +documentation for the target version. diff --git a/gitlab/config.go b/gitlab/config.go new file mode 100644 index 0000000..6619617 --- /dev/null +++ b/gitlab/config.go @@ -0,0 +1,375 @@ +package gitlab + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" +) + +const ( + DefaultBackupPath = "/var/opt/gitlab/backups" + DefaultConfigDir = "/etc/gitlab" + DefaultBackupBin = "gitlab-backup" +) + +var DefaultConfigPaths = []string{ + "/etc/gitlab/gitlab.rb", + "/etc/gitlab/gitlab-secrets.json", +} + +type Config struct { + Proto string + Location string + BackupPath string + ConfigPaths []string + ConfigDir string + BackupBin string + UseSudo bool + SSHHost string + SSHUser string + SSHPort string + SSHIdentity string + SSHBin string + RestoreForce bool +} + +func NewConfig(proto string, config map[string]string) (Config, error) { + cfg := Config{ + Proto: proto, + Location: config["location"], + BackupPath: valueOrDefault(config["backup_path"], DefaultBackupPath), + ConfigPaths: DefaultConfigPaths, + ConfigDir: valueOrDefault(config["config_dir"], DefaultConfigDir), + BackupBin: valueOrDefault(config["gitlab_backup_bin"], DefaultBackupBin), + SSHHost: config["ssh_host"], + SSHUser: config["ssh_user"], + SSHPort: config["ssh_port"], + SSHIdentity: config["ssh_identity_file"], + SSHBin: valueOrDefault(config["ssh_bin"], "ssh"), + } + + if v := strings.TrimSpace(config["config_paths"]); v != "" { + cfg.ConfigPaths = splitList(v) + } + + var err error + if cfg.UseSudo, err = parseBool(config["use_sudo"], false); err != nil { + return cfg, fmt.Errorf("invalid use_sudo: %w", err) + } + if cfg.RestoreForce, err = parseBool(config["force"], false); err != nil { + return cfg, fmt.Errorf("invalid force: %w", err) + } + if cfg.SSHPort != "" { + if _, err := strconv.Atoi(cfg.SSHPort); err != nil { + return cfg, fmt.Errorf("invalid ssh_port: %w", err) + } + } + if cfg.BackupPath == "" { + return cfg, fmt.Errorf("backup_path cannot be empty") + } + if cfg.ConfigDir == "" { + return cfg, fmt.Errorf("config_dir cannot be empty") + } + return cfg, nil +} + +func valueOrDefault(value, fallback string) string { + if strings.TrimSpace(value) == "" { + return fallback + } + return value +} + +func splitList(value string) []string { + parts := strings.Split(value, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + out = append(out, part) + } + } + return out +} + +func parseBool(value string, fallback bool) (bool, error) { + if strings.TrimSpace(value) == "" { + return fallback, nil + } + return strconv.ParseBool(value) +} + +func (c Config) remote() bool { + return c.SSHHost != "" +} + +func (c Config) Origin() string { + if c.remote() { + if c.SSHUser != "" { + return c.Proto + "+ssh://" + c.SSHUser + "@" + c.SSHHost + } + return c.Proto + "+ssh://" + c.SSHHost + } + return c.Proto + "://local" +} + +func (c Config) Ping(ctx context.Context) error { + if c.remote() { + _, err := c.runRemote(ctx, "command -v "+shellQuote(c.BackupBin)+" >/dev/null") + return err + } + _, err := exec.LookPath(c.BackupBin) + return err +} + +func (c Config) CreateBackup(ctx context.Context) (string, error) { + if c.remote() { + create := shellJoin(c.commandArgs(c.BackupBin, "create")) + script := "set -e\n" + + create + " >/dev/null\n" + + "ls -1t -- " + shellQuote(c.BackupPath) + "/*_gitlab_backup.tar | head -n 1" + out, err := c.runRemote(ctx, script) + if err != nil { + return "", err + } + path := strings.TrimSpace(string(out)) + if path == "" { + return "", fmt.Errorf("gitlab-backup create did not produce a backup path") + } + return path, nil + } + + cmd := exec.CommandContext(ctx, c.commandArgs(c.BackupBin, "create")[0], c.commandArgs(c.BackupBin, "create")[1:]...) + out, err := cmd.CombinedOutput() + if err != nil { + return "", commandError(err, out) + } + return newestBackup(c.BackupPath) +} + +func newestBackup(dir string) (string, error) { + matches, err := filepath.Glob(filepath.Join(dir, "*_gitlab_backup.tar")) + if err != nil { + return "", err + } + if len(matches) == 0 { + return "", fmt.Errorf("no GitLab backup tar found in %s", dir) + } + sort.Slice(matches, func(i, j int) bool { + left, lerr := os.Stat(matches[i]) + right, rerr := os.Stat(matches[j]) + if lerr != nil || rerr != nil { + return matches[i] > matches[j] + } + return left.ModTime().After(right.ModTime()) + }) + return matches[0], nil +} + +func (c Config) OpenPath(ctx context.Context, path string) (io.ReadCloser, error) { + if c.remote() { + return c.remoteReader(ctx, shellJoin(c.commandArgs("cat", "--", path))) + } + if c.UseSudo { + return c.localReader(ctx, c.commandArgs("cat", "--", path)...) + } + return os.Open(path) +} + +func (c Config) PathExists(ctx context.Context, path string) bool { + if c.remote() { + _, err := c.runRemote(ctx, shellJoin(c.commandArgs("test", "-r", path))) + return err == nil + } + _, err := os.Stat(path) + return err == nil +} + +func (c Config) WritePath(ctx context.Context, dst string, src io.Reader, mode os.FileMode) error { + if c.remote() { + dir := filepath.ToSlash(filepath.Dir(dst)) + if c.UseSudo { + script := "set -e\n" + shellJoin(c.commandArgs("mkdir", "-p", "--", dir)) + "\n" + + "sudo -n tee " + shellQuote(dst) + " >/dev/null" + return c.runRemoteWithStdin(ctx, script, src) + } + script := "set -e\nmkdir -p -- " + shellQuote(dir) + "\ncat > " + shellQuote(dst) + return c.runRemoteWithStdin(ctx, script, src) + } + if c.UseSudo { + tmp, err := os.CreateTemp("", "plakar-gitlab-*") + if err != nil { + return err + } + tmpName := tmp.Name() + defer os.Remove(tmpName) + if _, err := io.Copy(tmp, src); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + if out, err := exec.CommandContext(ctx, "sudo", "-n", "install", "-m", fmt.Sprintf("%04o", mode.Perm()), "-D", tmpName, dst).CombinedOutput(); err != nil { + return commandError(err, out) + } + return nil + } + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, src) + return err +} + +func (c Config) Restore(ctx context.Context, backupID string) error { + if backupID == "" { + return fmt.Errorf("missing GitLab backup id") + } + args := c.commandArgs(c.BackupBin, "restore", "BACKUP="+backupID) + if c.RestoreForce { + args = append(args, "force=yes") + } + if c.remote() { + _, err := c.runRemote(ctx, shellJoin(args)) + return err + } + out, err := exec.CommandContext(ctx, args[0], args[1:]...).CombinedOutput() + if err != nil { + return commandError(err, out) + } + return nil +} + +func (c Config) commandArgs(name string, args ...string) []string { + out := make([]string, 0, len(args)+3) + if c.UseSudo { + out = append(out, "sudo", "-n") + } + out = append(out, name) + out = append(out, args...) + return out +} + +func (c Config) sshArgs(script string) ([]string, error) { + if c.SSHHost == "" { + return nil, fmt.Errorf("ssh_host is required for remote operation") + } + args := []string{"-o", "BatchMode=yes"} + if c.SSHPort != "" { + args = append(args, "-p", c.SSHPort) + } + if c.SSHIdentity != "" { + args = append(args, "-i", c.SSHIdentity) + } + target := c.SSHHost + if c.SSHUser != "" { + target = c.SSHUser + "@" + target + } + args = append(args, target, script) + return args, nil +} + +func (c Config) runRemote(ctx context.Context, script string) ([]byte, error) { + args, err := c.sshArgs(script) + if err != nil { + return nil, err + } + out, err := exec.CommandContext(ctx, c.SSHBin, args...).CombinedOutput() + if err != nil { + return nil, commandError(err, out) + } + return out, nil +} + +func (c Config) runRemoteWithStdin(ctx context.Context, script string, stdin io.Reader) error { + args, err := c.sshArgs(script) + if err != nil { + return err + } + cmd := exec.CommandContext(ctx, c.SSHBin, args...) + cmd.Stdin = stdin + out, err := cmd.CombinedOutput() + if err != nil { + return commandError(err, out) + } + return nil +} + +func (c Config) remoteReader(ctx context.Context, script string) (io.ReadCloser, error) { + args, err := c.sshArgs(script) + if err != nil { + return nil, err + } + return startCommandReader(ctx, c.SSHBin, args...) +} + +func (c Config) localReader(ctx context.Context, args ...string) (io.ReadCloser, error) { + if len(args) == 0 { + return nil, fmt.Errorf("missing command") + } + return startCommandReader(ctx, args[0], args[1:]...) +} + +type commandReader struct { + io.ReadCloser + cmd *exec.Cmd + stderr *bytes.Buffer +} + +func startCommandReader(ctx context.Context, name string, args ...string) (io.ReadCloser, error) { + cmd := exec.CommandContext(ctx, name, args...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + return &commandReader{ReadCloser: stdout, cmd: cmd, stderr: &stderr}, nil +} + +func (r *commandReader) Close() error { + _ = r.ReadCloser.Close() + if err := r.cmd.Wait(); err != nil { + return commandError(err, r.stderr.Bytes()) + } + return nil +} + +func commandError(err error, out []byte) error { + msg := strings.TrimSpace(string(out)) + if msg == "" { + return err + } + return fmt.Errorf("%w: %s", err, msg) +} + +func shellQuote(value string) string { + if value == "" { + return "''" + } + return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'" +} + +func shellJoin(args []string) string { + parts := make([]string, len(args)) + for i, arg := range args { + parts[i] = shellQuote(arg) + } + return strings.Join(parts, " ") +} diff --git a/gitlab/config_test.go b/gitlab/config_test.go new file mode 100644 index 0000000..dc39460 --- /dev/null +++ b/gitlab/config_test.go @@ -0,0 +1,91 @@ +package gitlab + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +func TestNewConfigDefaults(t *testing.T) { + cfg, err := NewConfig("gitlab", map[string]string{"location": "gitlab://local"}) + if err != nil { + t.Fatal(err) + } + if cfg.BackupPath != DefaultBackupPath { + t.Fatalf("BackupPath=%q, want %q", cfg.BackupPath, DefaultBackupPath) + } + if cfg.ConfigDir != DefaultConfigDir { + t.Fatalf("ConfigDir=%q, want %q", cfg.ConfigDir, DefaultConfigDir) + } + if cfg.BackupBin != DefaultBackupBin { + t.Fatalf("BackupBin=%q, want %q", cfg.BackupBin, DefaultBackupBin) + } + if cfg.remote() { + t.Fatal("default config should be local") + } +} + +func TestNewConfigParsesSSHAndLists(t *testing.T) { + cfg, err := NewConfig("gitlab", map[string]string{ + "ssh_host": "gitlab.example.com", + "ssh_user": "git", + "ssh_port": "2222", + "use_sudo": "true", + "config_paths": "/etc/gitlab/gitlab.rb, /etc/gitlab/gitlab-secrets.json", + }) + if err != nil { + t.Fatal(err) + } + if !cfg.remote() { + t.Fatal("expected remote config") + } + if !cfg.UseSudo { + t.Fatal("expected sudo mode") + } + if len(cfg.ConfigPaths) != 2 { + t.Fatalf("ConfigPaths length=%d, want 2", len(cfg.ConfigPaths)) + } +} + +func TestShellQuote(t *testing.T) { + got := shellQuote("a'b c") + want := "'a'\"'\"'b c'" + if got != want { + t.Fatalf("shellQuote=%q, want %q", got, want) + } +} + +func TestNewestBackup(t *testing.T) { + dir := t.TempDir() + oldPath := filepath.Join(dir, "1700000000_gitlab_backup.tar") + newPath := filepath.Join(dir, "1800000000_gitlab_backup.tar") + if err := os.WriteFile(oldPath, []byte("old"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(newPath, []byte("new"), 0o600); err != nil { + t.Fatal(err) + } + oldTime := time.Now().Add(-time.Hour) + newTime := time.Now() + if err := os.Chtimes(oldPath, oldTime, oldTime); err != nil { + t.Fatal(err) + } + if err := os.Chtimes(newPath, newTime, newTime); err != nil { + t.Fatal(err) + } + got, err := newestBackup(dir) + if err != nil { + t.Fatal(err) + } + if got != newPath { + t.Fatalf("newestBackup=%q, want %q", got, newPath) + } +} + +func TestBackupIDFromFilename(t *testing.T) { + result := backupIDFromArchiveName("/backup/1700000000_2026_06_02_18.11.0_gitlab_backup.tar") + if result != "1700000000_2026_06_02_18.11.0" { + t.Fatalf("backup id=%q", result) + } +} diff --git a/gitlab/exporter.go b/gitlab/exporter.go new file mode 100644 index 0000000..4a22895 --- /dev/null +++ b/gitlab/exporter.go @@ -0,0 +1,88 @@ +package gitlab + +import ( + "context" + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/PlakarKorp/kloset/connectors" + eexporter "github.com/PlakarKorp/kloset/connectors/exporter" + "github.com/PlakarKorp/kloset/location" +) + +type Exporter struct { + config Config + backupID string +} + +func NewExporter(_ context.Context, _ *connectors.Options, proto string, config map[string]string) (eexporter.Exporter, error) { + cfg, err := NewConfig(proto, config) + if err != nil { + return nil, err + } + return &Exporter{config: cfg}, nil +} + +func (e *Exporter) Origin() string { return e.config.Origin() } +func (e *Exporter) Type() string { return e.config.Proto } +func (e *Exporter) Root() string { return "/" } +func (e *Exporter) Flags() location.Flags { return 0 } +func (e *Exporter) Ping(ctx context.Context) error { return e.config.Ping(ctx) } +func (e *Exporter) Close(_ context.Context) error { return nil } + +func (e *Exporter) Export(ctx context.Context, records <-chan *connectors.Record, results chan<- *connectors.Result) error { + defer close(results) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case record, ok := <-records: + if !ok { + if e.backupID == "" { + return fmt.Errorf("no GitLab backup archive found in snapshot") + } + return e.config.Restore(ctx, e.backupID) + } + results <- e.restoreRecord(ctx, record) + } + } +} + +func (e *Exporter) restoreRecord(ctx context.Context, record *connectors.Record) *connectors.Result { + if record.FileInfo.Lmode.IsDir() || record.Pathname == "manifest.json" { + return record.Ok() + } + if record.Reader == nil { + return record.Error(fmt.Errorf("record %s has no reader", record.Pathname)) + } + + base := path.Base(record.Pathname) + switch { + case strings.HasSuffix(base, "_gitlab_backup.tar"): + dst := filepath.Join(e.config.BackupPath, base) + if err := e.config.WritePath(ctx, dst, record.Reader, 0o600); err != nil { + return record.Error(fmt.Errorf("writing backup archive %s: %w", dst, err)) + } + e.backupID = backupIDFromArchiveName(base) + return record.Ok() + + case strings.HasPrefix(strings.TrimPrefix(record.Pathname, "/"), "config/"): + dst := filepath.Join(e.config.ConfigDir, base) + if err := e.config.WritePath(ctx, dst, record.Reader, 0o600); err != nil { + return record.Error(fmt.Errorf("writing GitLab config %s: %w", dst, err)) + } + return record.Ok() + + default: + return record.Error(fmt.Errorf("unexpected GitLab snapshot file: %s", record.Pathname)) + } +} + +func backupIDFromArchiveName(name string) string { + return strings.TrimSuffix(path.Base(name), "_gitlab_backup.tar") +} + +var _ eexporter.Exporter = (*Exporter)(nil) diff --git a/gitlab/exporter/main.go b/gitlab/exporter/main.go new file mode 100644 index 0000000..d4080be --- /dev/null +++ b/gitlab/exporter/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + + sdk "github.com/PlakarKorp/go-kloset-sdk" + gitlab "github.com/PlakarKorp/integration-gitlab" +) + +func main() { + sdk.EntrypointExporter(os.Args, gitlab.NewExporter) +} diff --git a/gitlab/exporter/schema.json b/gitlab/exporter/schema.json new file mode 100644 index 0000000..7923ce3 --- /dev/null +++ b/gitlab/exporter/schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Plakar GitLab CE Exporter Connector Config", + "type": "object", + "additionalProperties": false, + "properties": { + "location": { + "type": "string", + "description": "GitLab restore target URI. Use gitlab://local for local operation, or set ssh_host for remote operation.", + "pattern": "^gitlab://.*$" + }, + "backup_path": { + "type": "string", + "minLength": 1, + "description": "Directory where backup archives should be restored before invoking gitlab-backup restore. Defaults to /var/opt/gitlab/backups." + }, + "config_dir": { + "type": "string", + "minLength": 1, + "description": "Directory where config records are restored. Defaults to /etc/gitlab." + }, + "gitlab_backup_bin": { + "type": "string", + "minLength": 1, + "description": "gitlab-backup binary name or absolute path. Defaults to gitlab-backup." + }, + "use_sudo": { + "type": "boolean", + "description": "Prefix local and remote GitLab commands and privileged writes with sudo -n. Defaults to false." + }, + "force": { + "type": "boolean", + "description": "Pass force=yes to gitlab-backup restore. Defaults to false." + }, + "ssh_host": { + "type": "string", + "minLength": 1, + "description": "Remote GitLab host. When set, commands run through ssh instead of locally." + }, + "ssh_user": { + "type": "string", + "minLength": 1, + "description": "SSH username for remote operation." + }, + "ssh_port": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "SSH port for remote operation." + }, + "ssh_identity_file": { + "type": "string", + "minLength": 1, + "description": "SSH private key path for remote operation." + }, + "ssh_bin": { + "type": "string", + "minLength": 1, + "description": "SSH binary name or absolute path. Defaults to ssh." + } + } +} diff --git a/gitlab/go.mod b/gitlab/go.mod new file mode 100644 index 0000000..d8cdfad --- /dev/null +++ b/gitlab/go.mod @@ -0,0 +1,39 @@ +module github.com/PlakarKorp/integration-gitlab + +go 1.25.5 + +require ( + github.com/PlakarKorp/go-kloset-sdk v1.1.0-beta.1.0.20260213124244-86554ea13bd5 + github.com/PlakarKorp/kloset v1.1.0-beta.6 +) + +require ( + github.com/PlakarKorp/integration-grpc v1.1.0-beta.3 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nickball/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/tink-crypto/tink-go/v2 v2.6.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.46.1 // indirect +) diff --git a/gitlab/go.sum b/gitlab/go.sum new file mode 100644 index 0000000..652facb --- /dev/null +++ b/gitlab/go.sum @@ -0,0 +1,187 @@ +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 h1:5BIUS5hwyLM298mOf8e8TEgD3cCYqc86uaJdQCYZo/o= +github.com/NickBall/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5/go.mod h1:w5D10RxC0NmPYxmQ438CC1S07zaC1zpvuNW7s5sUk2Q= +github.com/PlakarKorp/go-cdc-chunkers v1.0.3 h1:6ozBFcNMHvGe6IsbPcAZUUEAExCgcNx3aa8xiCA6+Qw= +github.com/PlakarKorp/go-cdc-chunkers v1.0.3/go.mod h1:y7ag92JABKPBDoSOPwedssQ5NIOgjRm4Mu6yTBpmUMY= +github.com/PlakarKorp/go-kloset-sdk v1.1.0-beta.1.0.20260213124244-86554ea13bd5 h1:ZcQLPdEdy2kgP3cb70QNuMCQyVi9Iso57aPVZscyPPA= +github.com/PlakarKorp/go-kloset-sdk v1.1.0-beta.1.0.20260213124244-86554ea13bd5/go.mod h1:ni69BgWur3+rHb7cOg/8JOKEMFh8J7tEPxTTSUhGNjE= +github.com/PlakarKorp/integration-grpc v1.1.0-beta.3 h1:u0n6Uyz7wqHOMoMYbfBLrcSrfAGqzcNQtcgkgYfU5TQ= +github.com/PlakarKorp/integration-grpc v1.1.0-beta.3/go.mod h1:lrkbUc9iT0jHiZ5wmB19wAhjDTMRO/VDhMJEt5trFlY= +github.com/PlakarKorp/kloset v1.1.0-beta.6 h1:n0X/chwflOvKUmJfLXDdbc5M2mqZLW3czPZBt9BjbCQ= +github.com/PlakarKorp/kloset v1.1.0-beta.6/go.mod h1:x9NCuBvaOh49/FR1Wm1+WNZU+4wTolTKhZVHKLA8SPk= +github.com/RaduBerinde/axisds v0.1.0 h1:YItk/RmU5nvlsv/awo2Fjx97Mfpt4JfgtEVAGPrLdz8= +github.com/RaduBerinde/axisds v0.1.0/go.mod h1:UHGJonU9z4YYGKJxSaC6/TNcLOBptpmM5m2Cksbnw0Y= +github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 h1:bsU8Tzxr/PNz75ayvCnxKZWEYdLMPDkUgticP4a4Bvk= +github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54/go.mod h1:0tr7FllbE9gJkHq7CVeeDDFAFKQVy5RnCSSNBOvdqbc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/crlib v0.0.0-20250110162118-b7c9be99e911 h1:X+r2Lb1qj0APqrxM8NhBD3X3JDM1Fe+u+WAvhvKuLdM= +github.com/cockroachdb/crlib v0.0.0-20250110162118-b7c9be99e911/go.mod h1:Gq51ZeKaFCXk6QwuGM0w1dnaOqc/F5zKT2zA9D6Xeac= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= +github.com/cockroachdb/pebble/v2 v2.1.4 h1:j9wPgMDbkErFdAKYFGhsoCcvzcjR+6zrJ4jhKtJ6bOk= +github.com/cockroachdb/pebble/v2 v2.1.4/go.mod h1:Reo1RTniv1UjVTAu/Fv74y5i3kJ5gmVrPhO9UtFiKn8= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8XTuY0PT9Ane9qZGul/p67vGYwl9BFI= +github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= +github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= +github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= +github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 h1:0lgqHvJWHLGW5TuObJrfyEi6+ASTKDBWikGvPqy9Yiw= +github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nickball/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5 h1:eQr2od6dyd9gCLYHgMX2TlAYQtMUpxK7S0nsZXyH0L8= +github.com/nickball/go-aes-key-wrap v0.0.0-20170929221519-1c3aa3e4dfc5/go.mod h1:1VYCE0dvZM9Y2q8kcAHdXZB6YwfrCUQDeSJ2DuIiA4k= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= +github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/gitlab/importer.go b/gitlab/importer.go new file mode 100644 index 0000000..31ccc5f --- /dev/null +++ b/gitlab/importer.go @@ -0,0 +1,100 @@ +package gitlab + +import ( + "context" + "fmt" + "io" + "os" + "path" + "path/filepath" + "time" + + "github.com/PlakarKorp/kloset/connectors" + iimporter "github.com/PlakarKorp/kloset/connectors/importer" + "github.com/PlakarKorp/kloset/location" + "github.com/PlakarKorp/kloset/objects" +) + +const importerFlags = location.FLAG_STREAM | location.FLAG_NEEDACK + +type Importer struct { + config Config +} + +func NewImporter(_ context.Context, _ *connectors.Options, proto string, config map[string]string) (iimporter.Importer, error) { + cfg, err := NewConfig(proto, config) + if err != nil { + return nil, err + } + return &Importer{config: cfg}, nil +} + +func (i *Importer) Origin() string { return i.config.Origin() } +func (i *Importer) Type() string { return i.config.Proto } +func (i *Importer) Root() string { return "/" } +func (i *Importer) Flags() location.Flags { return importerFlags } +func (i *Importer) Ping(ctx context.Context) error { return i.config.Ping(ctx) } +func (i *Importer) Close(_ context.Context) error { return nil } +func (i *Importer) Import(ctx context.Context, records chan<- *connectors.Record, results <-chan *connectors.Result) error { + defer close(records) + + backupPath, err := i.config.CreateBackup(ctx) + if err != nil { + return fmt.Errorf("creating GitLab backup: %w", err) + } + if err := i.emitFile(ctx, records, results, backupPath, "/backup/"+path.Base(backupPath)); err != nil { + return err + } + for _, configPath := range i.config.ConfigPaths { + if err := i.emitOptionalFile(ctx, records, results, configPath, "/config/"+path.Base(configPath)); err != nil { + return err + } + } + return nil +} + +func (i *Importer) emitOptionalFile(ctx context.Context, records chan<- *connectors.Record, results <-chan *connectors.Result, sourcePath, snapshotPath string) error { + if !i.config.PathExists(ctx, sourcePath) { + return nil + } + return i.emitFile(ctx, records, results, sourcePath, snapshotPath) +} + +func (i *Importer) emitFile(ctx context.Context, records chan<- *connectors.Record, results <-chan *connectors.Result, sourcePath, snapshotPath string) error { + now := time.Now().UTC() + size := int64(0) + if !i.config.remote() { + if info, err := os.Stat(sourcePath); err == nil { + size = info.Size() + now = info.ModTime() + } + } + + fileinfo := objects.FileInfo{ + Lname: filepath.Base(sourcePath), + Lsize: size, + Lmode: 0o444, + LmodTime: now, + } + record := connectors.NewRecord(snapshotPath, "", fileinfo, nil, func() (io.ReadCloser, error) { + return i.config.OpenPath(ctx, sourcePath) + }) + + select { + case <-ctx.Done(): + return ctx.Err() + case records <- record: + } + + select { + case <-ctx.Done(): + return ctx.Err() + case result := <-results: + if result != nil && result.Err != nil { + return result.Err + } + return nil + } +} + +var _ iimporter.Importer = (*Importer)(nil) diff --git a/gitlab/importer/main.go b/gitlab/importer/main.go new file mode 100644 index 0000000..4d79e59 --- /dev/null +++ b/gitlab/importer/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + + sdk "github.com/PlakarKorp/go-kloset-sdk" + gitlab "github.com/PlakarKorp/integration-gitlab" +) + +func main() { + sdk.EntrypointImporter(os.Args, gitlab.NewImporter) +} diff --git a/gitlab/importer/schema.json b/gitlab/importer/schema.json new file mode 100644 index 0000000..500f1a6 --- /dev/null +++ b/gitlab/importer/schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Plakar GitLab CE Importer Connector Config", + "type": "object", + "additionalProperties": false, + "properties": { + "location": { + "type": "string", + "description": "GitLab source URI. Use gitlab://local for local operation, or set ssh_host for remote operation.", + "pattern": "^gitlab://.*$" + }, + "backup_path": { + "type": "string", + "minLength": 1, + "description": "Directory where gitlab-backup create writes backup archives. Defaults to /var/opt/gitlab/backups." + }, + "config_paths": { + "type": "string", + "minLength": 1, + "description": "Comma-separated GitLab config files to include. Defaults to /etc/gitlab/gitlab.rb,/etc/gitlab/gitlab-secrets.json." + }, + "config_dir": { + "type": "string", + "minLength": 1, + "description": "Base config directory used for restore. Defaults to /etc/gitlab." + }, + "gitlab_backup_bin": { + "type": "string", + "minLength": 1, + "description": "gitlab-backup binary name or absolute path. Defaults to gitlab-backup." + }, + "use_sudo": { + "type": "boolean", + "description": "Prefix local and remote GitLab commands and privileged reads with sudo -n. Defaults to false." + }, + "ssh_host": { + "type": "string", + "minLength": 1, + "description": "Remote GitLab host. When set, commands run through ssh instead of locally." + }, + "ssh_user": { + "type": "string", + "minLength": 1, + "description": "SSH username for remote operation." + }, + "ssh_port": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "SSH port for remote operation." + }, + "ssh_identity_file": { + "type": "string", + "minLength": 1, + "description": "SSH private key path for remote operation." + }, + "ssh_bin": { + "type": "string", + "minLength": 1, + "description": "SSH binary name or absolute path. Defaults to ssh." + } + } +} diff --git a/gitlab/manifest.yaml b/gitlab/manifest.yaml new file mode 100644 index 0000000..256bee4 --- /dev/null +++ b/gitlab/manifest.yaml @@ -0,0 +1,21 @@ +name: gitlab +display_name: GitLab CE +description: Integration providing backup and restore capabilities for self-hosted GitLab CE using GitLab native backup tooling. +api_version: v1.1.0 +homepage: https://github.com/PlakarKorp/integration-gitlab +license: ISC +tier: official +contact: mailto:help@plakar.io +connectors: + - type: importer + executable: gitlabImporter + protocols: [gitlab] + validator: ./importer/schema.json + class: devops + subclass: gitlab + - type: exporter + executable: gitlabExporter + protocols: [gitlab] + validator: ./exporter/schema.json + class: devops + subclass: gitlab From 5334d85ac9bdf6584ba04b2ada998aef0d784b89 Mon Sep 17 00:00:00 2001 From: Cosmic-Skye <7584038+thecosmicskye@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:09:21 -0400 Subject: [PATCH 2/2] Add GitLab integration license and helper tests --- gitlab/LICENSE | 13 +++++++++++++ gitlab/config_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 gitlab/LICENSE diff --git a/gitlab/LICENSE b/gitlab/LICENSE new file mode 100644 index 0000000..101a44a --- /dev/null +++ b/gitlab/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2026 TinyOps Studio LLC + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/gitlab/config_test.go b/gitlab/config_test.go index dc39460..38e851f 100644 --- a/gitlab/config_test.go +++ b/gitlab/config_test.go @@ -56,6 +56,37 @@ func TestShellQuote(t *testing.T) { } } +func TestCommandArgsWithSudo(t *testing.T) { + cfg := Config{UseSudo: true} + got := cfg.commandArgs("gitlab-backup", "restore", "BACKUP=abc") + want := []string{"sudo", "-n", "gitlab-backup", "restore", "BACKUP=abc"} + if len(got) != len(want) { + t.Fatalf("command length=%d, want %d: %#v", len(got), len(want), got) + } + for idx := range want { + if got[idx] != want[idx] { + t.Fatalf("command[%d]=%q, want %q", idx, got[idx], want[idx]) + } + } +} + +func TestSSHArgs(t *testing.T) { + cfg := Config{SSHHost: "gitlab.example.com", SSHUser: "git", SSHPort: "2222", SSHIdentity: "/tmp/key"} + got, err := cfg.sshArgs("true") + if err != nil { + t.Fatal(err) + } + want := []string{"-o", "BatchMode=yes", "-p", "2222", "-i", "/tmp/key", "git@gitlab.example.com", "true"} + if len(got) != len(want) { + t.Fatalf("ssh args length=%d, want %d: %#v", len(got), len(want), got) + } + for idx := range want { + if got[idx] != want[idx] { + t.Fatalf("sshArgs[%d]=%q, want %q", idx, got[idx], want[idx]) + } + } +} + func TestNewestBackup(t *testing.T) { dir := t.TempDir() oldPath := filepath.Join(dir, "1700000000_gitlab_backup.tar")