diff --git a/.dagger/README.md b/.dagger/README.md index 5655ae683..f1d8ee7f2 100644 --- a/.dagger/README.md +++ b/.dagger/README.md @@ -1,101 +1,103 @@ -# πŸ› οΈ Harbor CLI Dagger Pipeline +# πŸ› οΈ Harbor CLI β€” Dagger Pipeline -We use [Dagger](https://dagger.io) to define a CI/CD pipeline for building, linting, and publishing the [Harbor CLI](https://github.com/goharbor/harbor-cli). -This README will help beginners understand how to use Dagger in local development and CI workflows. +We use [Dagger](https://dagger.io) to define a **modular and reproducible CI/CD pipeline** for building, linting, testing, and publishing the [Harbor CLI](https://github.com/goharbor/harbor-cli). +This README provides a clear reference for contributors and maintainers to understand, run, and extend the pipeline locally or in CI. -## Prerequisites - -Before you start, ensure you have the following: - -1. Dagger: Install the latest version of Dagger. You can check the official documentation for installation steps: [Dagger Installation Guide](https://docs.dagger.io/install). - -## Dagger Setup and Development Mode +--- -### Run Dagger Develop +## 🚧 Prerequisites -```bash -dagger develop -``` +Before using the pipeline, make sure you have: -This command will generate the necessary files and configuration for building and running Dagger. +1. **Dagger CLI** β€” Install the latest version from the official docs: + πŸ‘‰ [Dagger Installation Guide](https://docs.dagger.io/install) +2. **Go** β€” Installed according to the version specified in the project’s `go.mod`. +3. **Docker** β€” Required if you’re publishing images. +--- -## πŸ“¦ Dagger Functions Explained +## βš™οΈ Setup and Development Mode -### πŸ”§ `BuildDev(platform)` +### Run Dagger in Development Mode -Builds a development binary for your target platform. +To start the Dagger session and enable live code reloads: ```bash -dagger call build-dev --platform="linux/amd64" export --path=bin/harbor-dev +dagger develop ``` -### 🧼 `LintReport()` +This command prepares the environment for pipeline development and local testing. -Runs `golangci-lint` on your code and saves the report to a file. +## πŸ“¦ Dagger Functions Overview -```bash -dagger call lint-report export --path=./LintReport.json -``` +| **Name** | **Description** | +|--------------------------------|-------------------------------------------------------------------------------------------------| +| `lint` | Runs `golangci-lint` and prints the report as a string to stdout. | +| `lint-report` | Runs `golangci-lint` and writes the lint report to a file. | +| `pipeline` | Executes the **full CI/CD pipeline** including build, test, lint, and publish stages. | +| `run-doc` | Generates CLI documentation and returns the directory containing generated files. | +| `test` | Runs all Go tests in the repository. | +| `test-report` | Executes Go tests and outputs a structured JSON test report. | +| `test-coverage` | Runs Go tests with coverage tracking. | +| `test-coverage-report` | Processes coverage data and returns a formatted Markdown report. | +| `vulnerability-check` | Runs `govulncheck` to detect known vulnerabilities in dependencies. | +| `vulnerability-check-report` | Runs `govulncheck` and saves results to a file (`vulnerability-check.report`). | +| `build-dev` | Create build of Harbor CLI for local testing and development| -### πŸ“ `TestCoverageReport()` +--- -Runs go test coverage tools and creates a report. -```bash -dagger call test-coverage-report export --path=coverage-report.md -``` +## 🧩 Example Usage -### βœ… `CheckCoverageThreshold(context, threshold)` +Below are some common commands to run specific Dagger functions locally: -Runs go test coverage tools and creates a report. The total coverage is compared to a threshold that can be set to e.g. 80%. ```bash -dagger call check-coverage-threshold --threshold 80.0 -``` +# Development build for binaries -### πŸš€ `PublishImage(registry, imageTags)` +dagger call build-dev --source=. --platform="linux/amd64" export --path=bin/harbor-dev -Builds and publishes the Harbor CLI image to the given container registry with proper OCI metadata labels. +# Print report to stdout +dagger call lint -Before running the command you have to export you registry password +# Save report to a file +dagger call lint-report export --path=LintReport.json -```shell -export REGPASS=Harbor12345 -``` +# Run Tests +dagger call test -```bash -dagger call publish-image \ - --registry=demo.goharbor.io \ - --registry-username=harbor-cli \ - --registry-password=env:REGPASS \ - --imageTags=v0.1.0,latest -``` +# Generate a JSON Report +dagger call test-report export --path=TestReport.json ---- +# Test Coverage +dagger call test-coverage -## βš™οΈ Configuration Constants +# Generate a Markdown Report +dagger call test-coverage-report export --path=coverage-report.md -Dagger uses these constant versions (you can modify them as needed): +# Vulnerability Check +dagger call vulnerability-check -```go -const ( - GO_VERSION = "1.24.2" - GOLANGCILINT_VERSION = "v2.1.2" - SYFT_VERSION = "v1.9.0" - GORELEASER_VERSION = "v2.3.2" -) +# Generate a Report +dagger call vulnerability-check-report export --path=vuln.report + +# Generate CLI docs +dagger call run-doc export --path=docs/cli ``` ---- -## πŸ’‘ Tips for Beginners +## πŸ’‘ Tips for Contributors -- Every container step is **reproducible** you can build locally or in GitHub Actions without changes. -- Use Dagger to cache Go builds and lint output, speeding up re-runs. +- Every step in Dagger is **deterministic and reproducible** β€” what you run locally is identical to CI. +- Use Dagger’s built-in caching to accelerate Go builds, lint runs, and dependency installs. +- Modular functions let you run only what you need, improving iteration speed and debugging efficiency. +- Prefer using `dagger develop` for fast iteration and testing new steps before committing. +- Store output reports (lint, test, coverage) under a consistent `/reports` directory for easier CI integration. +- If you modify or add new pipeline steps, document them under **Dagger Functions Overview** to maintain clarity. +- Always validate pipelines with `dagger call pipeline` locally before merging into main. --- ## πŸ“š References -- [Dagger Go SDK Docs](https://pkg.go.dev/dagger.io/dagger) -- [golangci-lint](https://golangci-lint.run/) -- [Goreleaser](https://goreleaser.com/) +- [Dagger Go SDK](https://pkg.go.dev/dagger.io/dagger) +- [golangci-lint Docs](https://golangci-lint.run/) +- [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) diff --git a/.dagger/apk.go b/.dagger/apk.go new file mode 100644 index 000000000..748ff9be5 --- /dev/null +++ b/.dagger/apk.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "fmt" + + "dagger/harbor-cli/internal/dagger" +) + +func (m *HarborCli) Apk(ctx context.Context, + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + source *dagger.Directory, +) (*dagger.Directory, error) { + if !m.IsInitialized { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + } + + buildfile := dag.File("APKBUILD", apkbuild(m.AppVersion)) + + archs := []struct { + Arch string + ApkArch string + }{ + {"arm64", "aarch64"}, + {"amd64", "x86_64"}, + } + + for _, arch := range archs { + filename := fmt.Sprintf("bin/harbor-cli_%s_linux_%s", m.AppVersion, arch.Arch) + binary := buildDir.File(filename) + + apk := dag.Container(dagger.ContainerOpts{ + Platform: dagger.Platform(fmt.Sprintf("linux/%s", arch.Arch)), + }). + From("alpine:3.19"). + WithExec([]string{"apk", "add", "--no-cache", "alpine-sdk", "abuild"}). + WithWorkdir("/build"). + WithFile("/build/harbor-cli", binary). + WithFile("/build/APKBUILD", buildfile). + + // create builder user + abuild group + WithExec([]string{"adduser", "-D", "builder"}). + WithExec([]string{"addgroup", "builder", "abuild"}). + WithExec([]string{"chown", "-R", "builder:builder", "/build"}). + + // switch to builder FIRST + WithUser("builder"). + WithEnvVariable("HOME", "/home/builder"). + + // generate signing key AS BUILDER (this is critical) + WithExec([]string{"abuild-keygen", "-a", "-n"}). + + // switch back to root ONLY to trust the public key + WithUser("root"). + WithExec([]string{ + "sh", "-c", + "mkdir -p /etc/apk/keys && cp /home/builder/.abuild/*.rsa.pub /etc/apk/keys/", + }). + + // back to builder for the build + WithUser("builder"). + WithEnvVariable("HOME", "/home/builder"). + + // sanity check (keep this until stable) + WithExec([]string{"sh", "-c", "ls -l ~/.abuild && echo HOME=$HOME"}). + + // run abuild + WithExec([]string{"abuild", "-rd"}) + + apkFile := apk. + Directory("/home/builder/packages"). + Directory(arch.ApkArch). + File(fmt.Sprintf("harbor-cli-%s-r0.apk", m.AppVersion)) + + apkFileName := fmt.Sprintf("apk/harbor-cli_%s_%s.apk", m.AppVersion, arch.Arch) + buildDir = buildDir.WithFile(apkFileName, apkFile) + } + + return buildDir, nil +} + +func apkbuild(ver string) string { + return fmt.Sprintf(`# APKBUILD +pkgname=harbor-cli +pkgver=%s +pkgrel=0 +pkgdesc="Harbor CLI β€” a command-line interface for interacting with your Harbor container registry." +url="https://github.com/goharbor/harbor-cli" +arch="x86_64 aarch64" +license="Apache-2.0" +depends="" +makedepends="" +source="" +maintainer="NucleoFusion " +builddir="/build" + +package() { + install -Dm755 "$builddir/harbor-cli" \ + "$pkgdir/usr/bin/harbor-cli" +} +`, ver) +} diff --git a/.dagger/apt.go b/.dagger/apt.go new file mode 100644 index 000000000..0d3ff9f4d --- /dev/null +++ b/.dagger/apt.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "fmt" + + "dagger/harbor-cli/internal/dagger" +) + +func (m *HarborCli) AptBuild(ctx context.Context, + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + source *dagger.Directory, + token *dagger.Secret, +) error { + if !m.IsInitialized { + err := m.init(ctx, source) + if err != nil { + return err + } + } + + archs := []string{"amd64", "arm64"} + root := dag.Directory() + root = root.WithDirectory("pool/main/m", buildDir.Directory("deb")) + githubToken, err := token.Plaintext(ctx) + if err != nil { + return err + } + + // Base container + container := dag.Container(). + From("debian:bookworm-slim"). + WithExec([]string{"apt-get", "update"}). + WithExec([]string{"apt-get", "install", "-y", "dpkg-dev", "gzip", "git"}). + WithEnvVariable("GH_TOKEN", githubToken). + WithMountedDirectory("/repo", root). + WithWorkdir("/repo") + + // Building `Package` file for each arch + for _, arch := range archs { + pkgDir := fmt.Sprintf("buildDirs/stable/main/binary-%s", arch) + poolDir := "pool/main/m" + + container = container.WithExec([]string{ + "bash", "-c", + fmt.Sprintf("mkdir -p %s && dpkg-scanpackages -a %s %s /dev/null > %s/Packages && gzip -9c %s/Packages > %s/Packages.gz && rm -rf %s/Packages", + pkgDir, arch, poolDir, pkgDir, pkgDir, pkgDir, pkgDir), + }) + } + + // Release File + container = container.WithExec([]string{ + "bash", "-c", + `cat < /repo/buildDirs/stable/Release +Origin: https://github.com/goharbor/harbor-cli +Label: HarborCLI +Suite: stable +Codename: stable +Architectures: amd64 arm64 +Components: main +Description: Harbor CLI β€” a command-line interface for interacting with your Harbor container registry. +EOF`, + }) + + container = container. + WithWorkdir("/repo"). + WithExec([]string{ + "bash", "-c", + fmt.Sprintf(` + set -e + cd /repo + + git init + git remote add origin https://x-access-token:$GH_TOKEN@github.com/nucleofusion/harbor-cli.git + git checkout -B gh-pages || git checkout --orphan gh-pages + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add buildDirs pool + + git commit -m "Update APT repo for %s" || echo "No changes to commit" + git push origin gh-pages -f + `, m.AppVersion), + }) + + _, err = container.Sync(ctx) + if err != nil { + return fmt.Errorf("failed to run container: %w", err) + } + + return nil +} + +// GH-PAGES Structure +// +// / +// β”œβ”€β”€ dist/ +// β”‚ └── stable/ +// β”‚ β”œβ”€β”€ Release +// β”‚ └── main/ +// β”‚ β”œβ”€β”€ binary-amd64/ +// β”‚ β”‚ └── Packages.gz +// β”‚ └── binary-arm64/ +// β”‚ └── Packages.gz +// └── pool/ +// └── main/ +// └── m/ +// β”œβ”€β”€ myapp_1.0.0_amd64.deb +// └── myapp_1.0.0_arm64.deb +// diff --git a/.dagger/archive.go b/.dagger/archive.go new file mode 100644 index 000000000..4bb53826c --- /dev/null +++ b/.dagger/archive.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "fmt" + + "dagger/harbor-cli/internal/dagger" +) + +func (m *HarborCli) Archive(ctx context.Context, + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + source *dagger.Directory, +) (*dagger.Directory, error) { + if !m.IsInitialized { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + } + + entries, err := buildDir.Entries(ctx) + if err != nil { + return nil, fmt.Errorf("could not read dist directory: %w", err) + } + if len(entries) == 0 { + return nil, fmt.Errorf("dist directory is empty β€” run build first") + } + + goos := []string{"linux", "darwin", "windows"} + goarch := []string{"amd64", "arm64"} + + archives := dag.Directory() + + for _, os := range goos { + for _, arch := range goarch { + binName := fmt.Sprintf("harbor-cli_%s_%s_%s", m.AppVersion, os, arch) + if os == "windows" { + binName += ".exe" + } + + binPath := fmt.Sprintf("bin/%s", binName) + + archiveName := fmt.Sprintf("harbor-cli_%s_%s_%s", m.AppVersion, os, arch) + archiveDir := getArchiveDirectory(buildDir.File(binPath), source) + + var ( + archiveFile string + container *dagger.Container + ) + + if os == "windows" { + // Handle Windows .zip + archiveFile = archiveName + ".zip" + container = dag.Container(). + From("alpine:latest"). + WithExec([]string{"apk", "add", "--no-cache", "zip"}). + WithMountedDirectory("/input", archiveDir). + WithMountedDirectory("/out", archives). + WithWorkdir("/input"). + WithExec([]string{"zip", "-j", "/out/" + archiveFile, "/input/harbor-cli", "/input/LICENSE", "/input/README.md"}) + } else { + archiveFile = archiveName + ".tar.gz" + container = dag.Container(). + From("alpine:latest"). + WithMountedDirectory("/input", archiveDir). + WithMountedDirectory("/out", archives). + WithWorkdir("/input"). + WithExec([]string{ + "tar", "-czf", "/out/" + archiveFile, "-C", "/input", ".", + }) + } + + archives = archives.WithFile(archiveFile, container.File("/out/"+archiveFile)) + } + } + + buildDir = buildDir.WithDirectory("archive", archives) + + return buildDir, nil +} + +func getArchiveDirectory(harborCli *dagger.File, source *dagger.Directory) *dagger.Directory { + archiveDir := dag.Directory() + + archiveDir = archiveDir. + WithFile("LICENSE", source.File("LICENSE")). + WithFile("README.md", source.File("README.md")). + WithFile("harbor-cli", harborCli) + + return archiveDir +} diff --git a/.dagger/build.go b/.dagger/build.go new file mode 100644 index 000000000..5c493ac80 --- /dev/null +++ b/.dagger/build.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "fmt" + "time" + + "dagger/harbor-cli/internal/dagger" +) + +func (m *HarborCli) Build(ctx context.Context, + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + source *dagger.Directory, +) (*dagger.Directory, error) { + if !m.IsInitialized { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + } + + goos := []string{"linux", "darwin", "windows"} + goarch := []string{"amd64", "arm64"} + + for _, os := range goos { + for _, arch := range goarch { + // Defining binary file name + binName := fmt.Sprintf("harbor-cli_%s_%s_%s", m.AppVersion, os, arch) + if os == "windows" { + binName += ".exe" + } + + builder := dag.Container(). + From("golang:"+m.GoVersion+"-alpine"). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithMountedDirectory("/src", source). + WithWorkdir("/src"). + WithEnvVariable("GOOS", os). + WithEnvVariable("GOARCH", arch) + + gitCommit, _ := builder.WithExec([]string{"git", "rev-parse", "--short", "HEAD", "--always"}).Stdout(ctx) + buildTime := time.Now().UTC().Format(time.RFC3339) + + ldflagsArgs := LDFlags(ctx, m.AppVersion, m.GoVersion, buildTime, gitCommit) + + builder = builder.WithExec([]string{ + "sh", "-c", + fmt.Sprintf(`go build -v -ldflags "%s" -o /bin/%s /src/cmd/harbor/main.go`, ldflagsArgs, binName), + }) + + file := builder.File("/bin/" + binName) // Taking file from container + buildDir = buildDir.WithFile(fmt.Sprintf("/bin/%s", binName), file) // Adding file(bin) to dist directory + } + } + + return buildDir, nil +} diff --git a/.dagger/buildDev.go b/.dagger/buildDev.go new file mode 100644 index 000000000..120a10c51 --- /dev/null +++ b/.dagger/buildDev.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "dagger/harbor-cli/internal/dagger" +) + +// Create build of Harbor CLI for local testing and development +func (m *HarborCli) BuildDev(ctx context.Context, platform string, source *dagger.Directory) *dagger.File { + err := m.init(ctx, source) + if err != nil { + return nil + } + + fmt.Println("πŸ› οΈ Building Harbor-Cli with Dagger...") + // Define the path for the binary output + os, arch, err := parsePlatform(platform) + if err != nil { + log.Fatalf("Error parsing platform: %v", err) + } + + builder := dag.Container(). + From("golang:"+m.GoVersion). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithMountedDirectory("/src", m.Source). // Ensure the source directory with go.mod is mounted + WithWorkdir("/src"). + WithEnvVariable("GOOS", os). + WithEnvVariable("GOARCH", arch) + + gitCommit, _ := builder.WithExec([]string{"git", "rev-parse", "--short", "HEAD", "--always"}).Stdout(ctx) + buildTime := time.Now().UTC().Format(time.RFC3339) + + ldflagsArgs := LDFlags(ctx, m.AppVersion, m.GoVersion, buildTime, gitCommit) + + builder = builder.WithExec([]string{ + "go", "build", "-ldflags", ldflagsArgs, "-o", "/bin/harbor-cli", "/src/cmd/harbor/main.go", + }) + return builder.File("/bin/harbor-cli") +} + +// Parse the platform string into os and arch +func parsePlatform(platform string) (string, string, error) { + parts := strings.Split(platform, "/") + if len(parts) != 2 { + return "", "", fmt.Errorf("invalid platform format: %s. Should be os/arch. E.g. darwin/amd64", platform) + } + + return parts[0], parts[1], nil +} diff --git a/.dagger/checksum.go b/.dagger/checksum.go new file mode 100644 index 000000000..b35699c2b --- /dev/null +++ b/.dagger/checksum.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "dagger/harbor-cli/internal/dagger" +) + +func (m *HarborCli) Checksum(ctx context.Context, + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + // +optional + source *dagger.Directory, +) (*dagger.Directory, error) { + sums := map[string]string{} + bins, err := DistBinaries(ctx, dag, buildDir) + if err != nil { + return nil, err + } + + shasum := dag.Container(). + From("alpine"). + WithMountedDirectory("/dist", buildDir). + WithWorkdir("/dist") + + for _, v := range bins { + // We Ignore the filepath provided, since it uses the directory structure, ie, + // archive/bin.tar.gz or rpm/harbor-cli.rpm + // And Instead when later merging I will strip the prefix + out, err := shasum.WithExec([]string{"sh", "-c", fmt.Sprintf("sha256sum %s | awk '{print $1}'", v)}).Stdout(ctx) + if err != nil { + return nil, err + } + + split := strings.Split(v, "/") + sums[out] = split[len(split)-1] // Taking only filename + } + + content := "" + for k, v := range sums { + content += fmt.Sprintf("%s %s\n", strings.TrimSuffix(k, "\n"), v) + } + + buildDir = buildDir.WithFile("checksums.txt", dag.File("checksums.txt", content)) + return buildDir, err +} diff --git a/.dagger/doc.go b/.dagger/doc.go new file mode 100644 index 000000000..bed9e9751 --- /dev/null +++ b/.dagger/doc.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + + "dagger/harbor-cli/internal/dagger" +) + +// Generate CLI Documentation and return the directory containing the generated files +func (m *HarborCli) RunDoc(ctx context.Context, source *dagger.Directory) (*dagger.Directory, error) { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + + return dag.Container(). + From("golang:"+m.GoVersion+"-alpine"). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src/doc"). + WithExec([]string{"go", "run", "doc.go"}). + WithExec([]string{"go", "run", "./man-docs/man_doc.go"}). + WithWorkdir("/src").Directory("/src/doc"), nil +} diff --git a/.dagger/go.mod b/.dagger/go.mod index efda7b819..b5044a395 100644 --- a/.dagger/go.mod +++ b/.dagger/go.mod @@ -1,51 +1,50 @@ module dagger/harbor-cli -go 1.24.4 +go 1.25.0 -replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 - -replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 - -replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.12.2 - -replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.12.2 +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 require ( - github.com/99designs/gqlgen v0.17.75 + github.com/99designs/gqlgen v0.17.81 github.com/Khan/genqlient v0.8.1 - github.com/vektah/gqlparser/v2 v2.5.28 - go.opentelemetry.io/otel v1.36.0 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 - go.opentelemetry.io/otel/log v0.12.2 - go.opentelemetry.io/otel/metric v1.36.0 - go.opentelemetry.io/otel/sdk v1.36.0 - go.opentelemetry.io/otel/sdk/log v0.12.2 - go.opentelemetry.io/otel/sdk/metric v1.36.0 - go.opentelemetry.io/otel/trace v1.36.0 - go.opentelemetry.io/proto/otlp v1.6.0 - golang.org/x/sync v0.15.0 - google.golang.org/grpc v1.73.0 + github.com/vektah/gqlparser/v2 v2.5.30 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 + go.opentelemetry.io/otel/log v0.14.0 + go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/sdk/log v0.14.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/proto/otlp v1.8.0 + golang.org/x/sync v0.17.0 + google.golang.org/grpc v1.76.0 ) require ( - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/sosodev/duration v1.3.1 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/protobuf v1.36.6 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 + +replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.14.0 + +replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.14.0 diff --git a/.dagger/go.sum b/.dagger/go.sum index 74758957c..03dea85c6 100644 --- a/.dagger/go.sum +++ b/.dagger/go.sum @@ -1,18 +1,16 @@ -github.com/99designs/gqlgen v0.17.75 h1:GwHJsptXWLHeY7JO8b7YueUI4w9Pom6wJTICosDtQuI= -github.com/99designs/gqlgen v0.17.75/go.mod h1:p7gbTpdnHyl70hmSpM8XG8GiKwmCv+T5zkdY8U8bLog= +github.com/99designs/gqlgen v0.17.81 h1:kCkN/xVyRb5rEQpuwOHRTYq83i0IuTQg9vdIiwEerTs= +github.com/99designs/gqlgen v0.17.81/go.mod h1:vgNcZlLwemsUhYim4dC1pvFP5FX0pr2Y+uYUoHFb1ig= github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -21,69 +19,71 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vektah/gqlparser/v2 v2.5.28 h1:bIulcl3LF69ba6EiZVGD88y4MkM+Jxrf3P2MX8xLRkY= -github.com/vektah/gqlparser/v2 v2.5.28/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +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/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= +github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= -go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= -go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0= -go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY= -go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= -go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +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/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +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/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +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= +go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= +go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +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/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/.dagger/lint.go b/.dagger/lint.go new file mode 100644 index 000000000..4c3eae88b --- /dev/null +++ b/.dagger/lint.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + + "dagger/harbor-cli/internal/dagger" +) + +// Runs golangci-lint and generates outputs the report as a file +func (m *HarborCli) LintReport(ctx context.Context, source *dagger.Directory) (*dagger.File, error) { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + + report := "golangci-lint.report" + return m.lint(ctx).WithExec([]string{ + "golangci-lint", "run", "-v", + "--output.tab.path=" + report, + "--issues-exit-code", "0", + }).File(report), nil +} + +// Runs golangci-lint and generates outputs the report as a string to stdout +func (m *HarborCli) Lint(ctx context.Context, source *dagger.Directory) (string, error) { + err := m.init(ctx, source) + if err != nil { + return "", err + } + + return m.lint(ctx).WithExec([]string{"golangci-lint", "run"}).Stderr(ctx) +} + +func (m *HarborCli) lint(_ context.Context) *dagger.Container { + fmt.Println("πŸ‘€ Running linter and printing results to file golangci-lint.txt.") + linter := dag.Container(). + From("golangci/golangci-lint:latest-alpine"). + WithMountedCache("/lint-cache", dag.CacheVolume("/lint-cache")). + WithEnvVariable("GOLANGCI_LINT_CACHE", "/lint-cache"). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src") + return linter +} diff --git a/.dagger/main.go b/.dagger/main.go index 117309856..4cc475e02 100644 --- a/.dagger/main.go +++ b/.dagger/main.go @@ -1,481 +1,47 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. package main import ( "context" "fmt" - "log" + "regexp" "strings" - "time" "dagger/harbor-cli/internal/dagger" ) -const ( - GOLANGCILINT_VERSION = "v2.1.2" - GO_VERSION = "1.25" - GORELEASER_VERSION = "v2.12.2" -) - -func New( - // Local or remote directory with source code, defaults to "./" - // +optional - // +defaultPath="./" - source *dagger.Directory, -) *HarborCli { - return &HarborCli{Source: source} -} - type HarborCli struct { - // Local or remote directory with source code, defaults to "./" - Source *dagger.Directory -} - -// Create build of Harbor CLI for local testing and development -func (m *HarborCli) BuildDev( - ctx context.Context, - platform string, -) *dagger.File { - fmt.Println("πŸ› οΈ Building Harbor-Cli with Dagger...") - // Define the path for the binary output - os, arch, err := parsePlatform(platform) - if err != nil { - log.Fatalf("Error parsing platform: %v", err) - } - builder := dag.Container(). - From("golang:"+GO_VERSION). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithMountedDirectory("/src", m.Source). // Ensure the source directory with go.mod is mounted - WithWorkdir("/src"). - WithEnvVariable("GOOS", os). - WithEnvVariable("GOARCH", arch) - - gitCommit, _ := builder.WithExec([]string{"git", "rev-parse", "--short", "HEAD", "--always"}).Stdout(ctx) - buildTime := time.Now().UTC().Format(time.RFC3339) - ldflagsArgs := fmt.Sprintf(`-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.Version=dev - -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GoVersion=%s - -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.BuildTime=%s - -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GitCommit=%s - `, GO_VERSION, buildTime, gitCommit) - builder = builder.WithExec([]string{ - "go", "build", "-ldflags", ldflagsArgs, "-o", "/bin/harbor-cli", "/src/cmd/harbor/main.go", - }) - return builder.File("/bin/harbor-cli") + Source *dagger.Directory // Source Directory where code resides + AppVersion string // Current Version of the app, acquired from git tags + GoVersion string // Go Version used in the current release, acquired from the go.mod file + IsInitialized bool } -// Return list of containers for list of oses and arches -// -// FIXME: there is a bug where you cannot return a list of containers right now -// this function works as expected because it is only called by other functions but -// calling it via the CLI results in an error. That is why this into a private function for -// now so that no one calls this https://github.com/dagger/dagger/issues/8202#issuecomment-2317291483 -func (m *HarborCli) build( - ctx context.Context, - version string, -) []*dagger.Container { - var builds []*dagger.Container - - fmt.Println("πŸ› οΈ Building with Dagger...") - oses := []string{"linux", "darwin", "windows"} - arches := []string{"amd64", "arm64"} - - // temp container with git installed - temp := dag.Container(). +func (m *HarborCli) init(ctx context.Context, source *dagger.Directory) error { + out, err := dag.Container(). From("alpine:latest"). - WithMountedDirectory("/src", m.Source). - // --no-cache option is to avoid caching the apk package index WithExec([]string{"apk", "add", "--no-cache", "git"}). - WithWorkdir("/src") - - gitCommit, _ := temp.WithExec([]string{"git", "rev-parse", "--short", "HEAD", "--always"}).Stdout(ctx) - buildTime := time.Now().UTC().Format(time.RFC3339) - ldflagsArgs := fmt.Sprintf(`-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.Version=%s - -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GoVersion=%s - -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.BuildTime=%s - -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GitCommit=%s - `, version, GO_VERSION, buildTime, gitCommit) - - for _, goos := range oses { - for _, goarch := range arches { - bin_path := fmt.Sprintf("build/%s/%s/", goos, goarch) - builder := dag.Container(). - From("golang:"+GO_VERSION+"-alpine"). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithMountedDirectory("/src", m.Source). - WithWorkdir("/src"). - WithEnvVariable("GOOS", goos). - WithEnvVariable("GOARCH", goarch). - WithExec([]string{"go", "build", "-ldflags", ldflagsArgs, "-o", bin_path + "harbor", "/src/cmd/harbor/main.go"}). - WithWorkdir(bin_path). - WithExec([]string{"ls"}). - WithEntrypoint([]string{"./harbor"}) - - builds = append(builds, builder) - } - } - return builds -} - -// Executes Linter and writes results to a file golangci-lint.report -func (m *HarborCli) LintReport(ctx context.Context) *dagger.File { - report := "golangci-lint.report" - return m.lint(ctx).WithExec([]string{ - "golangci-lint", "run", "-v", - "--output.tab.path=" + report, - "--issues-exit-code", "0", - }).File(report) -} - -// Lint Run the linter golangci-lint -func (m *HarborCli) Lint(ctx context.Context) (string, error) { - return m.lint(ctx).WithExec([]string{"golangci-lint", "run"}).Stderr(ctx) -} - -func (m *HarborCli) lint(_ context.Context) *dagger.Container { - fmt.Println("πŸ‘€ Running linter and printing results to file golangci-lint.txt.") - linter := dag.Container(). - From("golangci/golangci-lint:"+GOLANGCILINT_VERSION+"-alpine"). - WithMountedCache("/lint-cache", dag.CacheVolume("/lint-cache")). - WithEnvVariable("GOLANGCI_LINT_CACHE", "/lint-cache"). - WithMountedDirectory("/src", m.Source). - WithWorkdir("/src") - return linter -} - -// PublishImage publishes a container image to a registry with a specific tag and signs it using Cosign. -func (m *HarborCli) PublishImage( - ctx context.Context, - registry, registryUsername string, - // +optional - // +default=["latest"] - imageTags []string, - registryPassword *dagger.Secret, -) []string { - version := getVersion(imageTags) - builders := m.build(ctx, version) - releaseImages := []*dagger.Container{} - - for i, tag := range imageTags { - imageTags[i] = strings.TrimSpace(tag) - if strings.HasPrefix(imageTags[i], "v") { - imageTags[i] = strings.TrimPrefix(imageTags[i], "v") - } - } - fmt.Printf("provided tags: %s\n", imageTags) - - // Get current time for image creation timestamp - creationTime := time.Now().UTC().Format(time.RFC3339) - - for _, builder := range builders { - os, _ := builder.EnvVariable(ctx, "GOOS") - arch, _ := builder.EnvVariable(ctx, "GOARCH") - - if os != "linux" { - continue - } - - ctr := dag.Container(dagger.ContainerOpts{Platform: dagger.Platform(os + "/" + arch)}). - From("alpine:latest"). - WithWorkdir("/"). - WithFile("/harbor", builder.File("./harbor")). - WithExec([]string{"ls", "-al"}). - WithExec([]string{"./harbor", "version"}). - // Add required metadata labels for ArtifactHub - WithLabel("org.opencontainers.image.created", creationTime). - WithLabel("org.opencontainers.image.description", "Harbor CLI - A command-line interface for CNCF Harbor, the cloud native registry!"). - WithLabel("io.artifacthub.package.readme-url", "https://raw.githubusercontent.com/goharbor/harbor-cli/main/README.md"). - WithLabel("org.opencontainers.image.source", "https://github.com/goharbor/harbor-cli"). - WithLabel("org.opencontainers.image.version", version). - WithLabel("io.artifacthub.package.license", "Apache-2.0"). - WithEntrypoint([]string{"/harbor"}) - releaseImages = append(releaseImages, ctr) - } - - imageAddrs := []string{} - for _, imageTag := range imageTags { - addr, err := dag.Container().WithRegistryAuth(registry, registryUsername, registryPassword). - Publish(ctx, - fmt.Sprintf("%s/%s/harbor-cli:%s", registry, "harbor-cli", imageTag), - dagger.ContainerPublishOpts{PlatformVariants: releaseImages}, - ) - if err != nil { - panic(err) - } - fmt.Printf("Published image address: %s\n", addr) - imageAddrs = append(imageAddrs, addr) - } - return imageAddrs -} - -// SnapshotRelease Create snapshot non OCI artifacts with goreleaser -func (m *HarborCli) SnapshotRelease(ctx context.Context) *dagger.Directory { - return m.goreleaserContainer(). - WithExec([]string{"goreleaser", "release", "--snapshot", "--clean"}). - Directory("/src/dist") -} - -// Release Create release with goreleaser -func (m *HarborCli) Release(ctx context.Context, githubToken *dagger.Secret) { - goreleaser := m.goreleaserContainer(). - WithSecretVariable("GITHUB_TOKEN", githubToken). - WithExec([]string{"goreleaser", "release", "--clean"}) - error, err := goreleaser.Stderr(ctx) - if err != nil { - log.Printf("Error occured during release: %s", err) - return - } - if len(error) > 0 { - log.Printf("Error occured while release: %s", err) - return - } - log.Println("Release tasks completed successfully πŸŽ‰") -} - -// Return a container with the goreleaser binary mounted and the source directory mounted. -func (m *HarborCli) goreleaserContainer() *dagger.Container { - return dag.Container(). - From(fmt.Sprintf("goreleaser/goreleaser:%s", GORELEASER_VERSION)). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithMountedDirectory("/src", m.Source). - WithWorkdir("/src") -} - -// Generate CLI Documentation and return the directory containing the generated files -func (m *HarborCli) RunDoc(ctx context.Context) *dagger.Directory { - return dag.Container(). - From("golang:"+GO_VERSION+"-alpine"). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithMountedDirectory("/src", m.Source). - WithWorkdir("/src/doc"). - WithExec([]string{"go", "run", "doc.go"}). - WithExec([]string{"go", "run", "./man-docs/man_doc.go"}). - WithWorkdir("/src").Directory("/src/doc") -} - -// Executes Go tests -func (m *HarborCli) Test(ctx context.Context) (string, error) { - test := dag.Container(). - From("golang:"+GO_VERSION+"-alpine"). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithMountedDirectory("/src", m.Source). - WithWorkdir("/src"). - WithExec([]string{"go", "test", "-v", "./..."}) - return test.Stdout(ctx) -} - -// Executes Go tests and returns TestReport in json file -// TestReport executes Go tests and returns only the JSON report file -func (m *HarborCli) TestReport(ctx context.Context) *dagger.File { - reportName := "TestReport.json" - test := dag.Container(). - From("golang:"+GO_VERSION+"-alpine"). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}). - WithMountedDirectory("/src", m.Source). - WithWorkdir("/src"). - WithExec([]string{"gotestsum", "--jsonfile", reportName, "./..."}) - - return test.File(reportName) -} - -func (m *HarborCli) TestCoverage(ctx context.Context) *dagger.File { - coverage := "coverage.out" - test := dag.Container(). - From("golang:"+GO_VERSION+"-alpine"). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}). - WithMountedDirectory("/src", m.Source). - WithWorkdir("/src"). - WithExec([]string{"gotestsum", "--", "-coverprofile=" + coverage, "./..."}) - - return test.File(coverage) -} - -// TestCoverageReport processes coverage data and returns a formatted markdown report -func (m *HarborCli) TestCoverageReport(ctx context.Context) *dagger.File { - coverageFile := "coverage.out" - reportFile := "coverage-report.md" - test := dag.Container(). - From("golang:"+GO_VERSION+"-alpine"). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithMountedDirectory("/src", m.Source). + WithMountedDirectory("/src", source). WithWorkdir("/src"). - WithExec([]string{"apk", "add", "--no-cache", "bc"}). - WithExec([]string{"go", "test", "-coverprofile=" + coverageFile, "./..."}) - return test.WithExec([]string{"sh", "-c", ` - echo "

πŸ“Š Test Coverage Results

" > ` + reportFile + ` - if [ ! -f "` + coverageFile + `" ]; then - echo "

❌ Coverage file not found!

" >> ` + reportFile + ` - exit 1 - fi - total_coverage=$(go tool cover -func=` + coverageFile + ` | grep total: | grep -Eo '[0-9]+\.[0-9]+') - echo "DEBUG: Total coverage is $total_coverage" >&2 - if (( $(echo "$total_coverage >= 80.0" | bc -l) )); then - emoji="βœ…" - elif (( $(echo "$total_coverage >= 60.0" | bc -l) )); then - emoji="⚠️" - else - emoji="❌" - fi - echo "

Total coverage: $emoji $total_coverage% (Target: 80%)

" >> ` + reportFile + ` - echo "
Detailed package coverage
" >> ` + reportFile + `
-        go tool cover -func=` + coverageFile + ` >> ` + reportFile + `
-        echo "
" >> ` + reportFile + ` - cat ` + reportFile + ` >&2 - `}).File(reportFile) -} - -// Checks for vulnerabilities using govulncheck -func (m *HarborCli) vulnerabilityCheck(ctx context.Context) *dagger.Container { - return dag.Container(). - From("golang:"+GO_VERSION+"-alpine"). - WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+GO_VERSION)). - WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). - WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+GO_VERSION)). - WithEnvVariable("GOCACHE", "/go/build-cache"). - WithExec([]string{"go", "install", "golang.org/x/vuln/cmd/govulncheck@latest"}). - WithMountedDirectory("/src", m.Source). - WithWorkdir("/src") -} - -// Runs a vulnerability check using govulncheck -func (m *HarborCli) VulnerabilityCheck(ctx context.Context) (string, error) { - return m.vulnerabilityCheck(ctx). - WithExec([]string{"govulncheck", "-show", "verbose", "./..."}). - Stderr(ctx) -} - -// Runs a vulnerability check using govulncheck and writes results to vulnerability-check.report -func (m *HarborCli) VulnerabilityCheckReport(ctx context.Context) *dagger.File { - report := "vulnerability-check.report" - cmd := fmt.Sprintf("govulncheck ./... > %s || true", report) - return m.vulnerabilityCheck(ctx). - WithExec([]string{ - "sh", "-c", cmd, - }).File(report) -} - -// Parse the platform string into os and arch -func parsePlatform(platform string) (string, string, error) { - parts := strings.Split(platform, "/") - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid platform format: %s. Should be os/arch. E.g. darwin/amd64", platform) - } - return parts[0], parts[1], nil - -} - -func getVersion(tags []string) string { - for _, tag := range tags { - if strings.HasPrefix(tag, "v") { - return tag - } + WithExec([]string{"git", "describe", "--tags", "--abbrev=0"}). + Stdout(ctx) + if err != nil { + return fmt.Errorf("failed to get version: %w", err) } - return "latest" -} -// PublishImageAndSign builds and publishes container images to a registry with a specific tags and then signs them using Cosign. -func (m *HarborCli) PublishImageAndSign( - ctx context.Context, - registry string, - registryUsername string, - registryPassword *dagger.Secret, - imageTags []string, - // +optional - githubToken *dagger.Secret, - // +optional - actionsIdTokenRequestToken *dagger.Secret, - // +optional - actionsIdTokenRequestUrl string, -) (string, error) { - imageAddrs := m.PublishImage(ctx, registry, registryUsername, imageTags, registryPassword) - _, err := m.Sign( - ctx, - githubToken, - actionsIdTokenRequestUrl, - actionsIdTokenRequestToken, - registryUsername, - registryPassword, - imageAddrs[0], - ) + goVersion, err := source.File("go.mod").Contents(ctx) if err != nil { - return "", fmt.Errorf("failed to sign image: %w", err) + return err } - fmt.Printf("Signed image: %s\n", imageAddrs) - return imageAddrs[0], nil -} - -// Sign signs a container image using Cosign, works also with GitHub Actions -func (m *HarborCli) Sign(ctx context.Context, - // +optional - githubToken *dagger.Secret, - // +optional - actionsIdTokenRequestUrl string, - // +optional - actionsIdTokenRequestToken *dagger.Secret, - registryUsername string, - registryPassword *dagger.Secret, - imageAddr string, -) (string, error) { - registryPasswordPlain, _ := registryPassword.Plaintext(ctx) - - cosing_ctr := dag.Container().From("cgr.dev/chainguard/cosign") - - // If githubToken is provided, use it to sign the image - if githubToken != nil { - if actionsIdTokenRequestUrl == "" || actionsIdTokenRequestToken == nil { - return "", fmt.Errorf("actionsIdTokenRequestUrl (exist=%s) and actionsIdTokenRequestToken (exist=%t) must be provided when githubToken is provided", actionsIdTokenRequestUrl, actionsIdTokenRequestToken != nil) - } - fmt.Printf("Setting the ENV Vars GITHUB_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN to sign with GitHub Token") - cosing_ctr = cosing_ctr.WithSecretVariable("GITHUB_TOKEN", githubToken). - WithEnvVariable("ACTIONS_ID_TOKEN_REQUEST_URL", actionsIdTokenRequestUrl). - WithSecretVariable("ACTIONS_ID_TOKEN_REQUEST_TOKEN", actionsIdTokenRequestToken) + re := regexp.MustCompile(`(?m)^go (\d+\.\d+(\.\d+)?)`) + match := re.FindStringSubmatch(goVersion) + if len(match) > 1 { + m.GoVersion = match[1] } - return cosing_ctr.WithSecretVariable("REGISTRY_PASSWORD", registryPassword). - WithExec([]string{"cosign", "env"}). - WithExec([]string{ - "cosign", "sign", "--yes", "--recursive", - "--registry-username", registryUsername, - "--registry-password", registryPasswordPlain, - imageAddr, - "--timeout", "1m", - }).Stdout(ctx) + m.Source = source + m.AppVersion = strings.TrimSpace(strings.TrimLeft(out, "v")) + m.IsInitialized = true + + return nil } diff --git a/.dagger/nfpm.go b/.dagger/nfpm.go new file mode 100644 index 000000000..860ee592c --- /dev/null +++ b/.dagger/nfpm.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "fmt" + + "dagger/harbor-cli/internal/dagger" +) + +func (m *HarborCli) NfpmBuild(ctx context.Context, + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + // +optional + source *dagger.Directory, +) (*dagger.Directory, error) { + if !m.IsInitialized { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + } + + archs := []string{"amd64", "arm64"} + pkgs := []string{"deb", "rpm"} + + for _, pkg := range pkgs { + out := dag.Directory() + for _, arch := range archs { + fileName := fmt.Sprintf("harbor-cli_%s_%s.%s", m.AppVersion, arch, pkg) + + out = TemplatedYML(out, arch, m.AppVersion, fmt.Sprintf("harbor-cli_%s_%s_%s", m.AppVersion, "linux", arch)) + + pkgFile := dag.Container(). + From("goreleaser/nfpm"). + WithMountedFile("/nfpm.yml", out.File("/nfpm.yml")). + WithMountedDirectory("/input", buildDir). + WithWorkdir("/input"). + WithExec([]string{ + "nfpm", + "pkg", + "--config", "/nfpm.yml", + "--packager", pkg, + "--target", fmt.Sprintf("/%s", fileName), + }). + File(fmt.Sprintf("/%s", fileName)) + + out = out.WithFile(fileName, pkgFile) + } + + buildDir = buildDir.WithDirectory(fmt.Sprintf("/%s", pkg), out) + } + + return buildDir, nil +} + +func TemplatedYML(out *dagger.Directory, arch string, appV string, filename string) *dagger.Directory { + out = out.WithNewFile("nfpm.yml", fmt.Sprintf(` +name: harbor-cli +arch: %s +platform: linux +version: %s +section: default +priority: extra +maintainer: "NucleoFusion " +description: "Harbor CLI β€” a command-line interface for interacting with your Harbor container registry." +license: Apache 2.0 +contents: + - src: ./bin/%s + dst: /usr/local/bin/harbor-cli +`, arch, appV, filename)) + + return out +} diff --git a/.dagger/publishimage.go b/.dagger/publishimage.go new file mode 100644 index 000000000..de06956a2 --- /dev/null +++ b/.dagger/publishimage.go @@ -0,0 +1,264 @@ +package main + +import ( + "context" + "fmt" + "strings" + "time" + + "dagger/harbor-cli/internal/dagger" +) + +func (m *HarborCli) PublishImageAndSign( + ctx context.Context, + // +optional + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + source *dagger.Directory, + registry string, + registryUsername string, + registryPassword *dagger.Secret, + imageTags string, + // +optional + githubToken *dagger.Secret, + // +optional + actionsIdTokenRequestToken *dagger.Secret, + // +optional + actionsIdTokenRequestUrl *dagger.Secret, +) (string, error) { + if !m.IsInitialized { + err := m.init(ctx, source) + if err != nil { + return "", err + } + } + + imageAddrs, err := m.PublishImage(ctx, registry, registryUsername, strings.Split(imageTags, ","), buildDir, source, registryPassword) + if err != nil { + return "", err + } + + _, err = m.Sign( + ctx, + githubToken, + actionsIdTokenRequestUrl, + actionsIdTokenRequestToken, + registryUsername, + registryPassword, + imageAddrs[0], + ) + if err != nil { + return "", fmt.Errorf("failed to sign image: %w", err) + } + + fmt.Printf("Signed image: %s\n", imageAddrs) + return imageAddrs[0], nil +} + +func (m *HarborCli) PublishImage( + ctx context.Context, + registry, registryUsername string, + // +optional + // +default=["latest"] + imageTags []string, + // +optional + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + source *dagger.Directory, + registryPassword *dagger.Secret, +) ([]string, error) { + if !m.IsInitialized { + err := m.init(ctx, source) + if err != nil { + return []string{}, err + } + } + + version := getVersion(imageTags) + releaseImages := []*dagger.Container{} + + for i, tag := range imageTags { + imageTags[i] = strings.TrimSpace(tag) + if strings.HasPrefix(imageTags[i], "v") { + imageTags[i] = strings.TrimPrefix(imageTags[i], "v") + } + } + fmt.Printf("provided tags: %s\n", imageTags) + + // Get current time for image creation timestamp + creationTime := time.Now().UTC().Format(time.RFC3339) + + // If the buildDir is not provided, build new binaries ones + if buildDir == nil { + buildDir = dag.Directory() + + builders := m.build(ctx, version) + + for _, builder := range builders { + os, _ := builder.EnvVariable(ctx, "GOOS") + arch, _ := builder.EnvVariable(ctx, "GOARCH") + + if os != "linux" { + continue + } + + ctr := dag.Container(dagger.ContainerOpts{Platform: dagger.Platform(os + "/" + arch)}). + From("alpine:latest"). + WithWorkdir("/"). + WithFile("/harbor", builder.File("./harbor")). + WithExec([]string{"ls", "-al"}). + WithExec([]string{"./harbor", "version"}). + // Add required metadata labels for ArtifactHub + WithLabel("org.opencontainers.image.created", creationTime). + WithLabel("org.opencontainers.image.description", "Harbor CLI - A command-line interface for CNCF Harbor, the cloud native registry!"). + WithLabel("io.artifacthub.package.readme-url", "https://raw.githubusercontent.com/goharbor/harbor-cli/main/README.md"). + WithLabel("org.opencontainers.image.source", "https://github.com/goharbor/harbor-cli"). + WithLabel("org.opencontainers.image.version", version). + WithLabel("io.artifacthub.package.license", "Apache-2.0"). + WithEntrypoint([]string{"/harbor"}) + + releaseImages = append(releaseImages, ctr) + } + } else { // If buildDir is provided, use existing binaries + archs := []string{"amd64", "arm64"} + + for _, arch := range archs { + filepath := fmt.Sprintf("bin/harbor-cli_%s_linux_%s", m.AppVersion, arch) + + ctr := dag.Container(dagger.ContainerOpts{Platform: dagger.Platform("linux/" + arch)}). + From("alpine:latest"). + WithWorkdir("/"). + WithFile("/harbor", buildDir.File(filepath)). + WithExec([]string{"ls", "-al"}). + WithExec([]string{"chmod", "+x", "/harbor"}). + WithExec([]string{"uname", "-m"}). + WithExec([]string{"./harbor", "version"}). + // Add required metadata labels for ArtifactHub + WithLabel("org.opencontainers.image.created", creationTime). + WithLabel("org.opencontainers.image.description", "Harbor CLI - A command-line interface for CNCF Harbor, the cloud native registry!"). + WithLabel("io.artifacthub.package.readme-url", "https://raw.githubusercontent.com/goharbor/harbor-cli/main/README.md"). + WithLabel("org.opencontainers.image.source", "https://github.com/goharbor/harbor-cli"). + WithLabel("org.opencontainers.image.version", version). + WithLabel("io.artifacthub.package.license", "Apache-2.0"). + WithEntrypoint([]string{"/harbor"}) + + releaseImages = append(releaseImages, ctr) + } + } + + imageAddrs := []string{} + for _, imageTag := range imageTags { + addr, err := dag.Container().WithRegistryAuth(registry, registryUsername, registryPassword). + Publish(ctx, + fmt.Sprintf("%s/%s/harbor-cli:%s", registry, registryUsername, imageTag), + dagger.ContainerPublishOpts{PlatformVariants: releaseImages}, + ) + if err != nil { + return []string{}, err + } + + fmt.Printf("Published image address: %s\n", addr) + imageAddrs = append(imageAddrs, addr) + } + + return imageAddrs, nil +} + +func (m *HarborCli) build( + ctx context.Context, + version string, +) []*dagger.Container { + var builds []*dagger.Container + + fmt.Println("πŸ› οΈ Building with Dagger...") + oses := []string{"linux", "darwin", "windows"} + arches := []string{"amd64", "arm64"} + + // temp container with git installed + temp := dag.Container(). + From("alpine:latest"). + WithMountedDirectory("/src", m.Source). + // --no-cache option is to avoid caching the apk package index + WithExec([]string{"apk", "add", "--no-cache", "git"}). + WithWorkdir("/src") + + gitCommit, _ := temp.WithExec([]string{"git", "rev-parse", "--short", "HEAD", "--always"}).Stdout(ctx) + buildTime := time.Now().UTC().Format(time.RFC3339) + ldflagsArgs := fmt.Sprintf(`-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.Version=%s + -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GoVersion=%s + -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.BuildTime=%s + -X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GitCommit=%s + `, version, m.GoVersion, buildTime, gitCommit) + + for _, goos := range oses { + for _, goarch := range arches { + bin_path := fmt.Sprintf("build/%s/%s/", goos, goarch) + builder := dag.Container(). + From("golang:"+m.GoVersion+"-alpine"). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithEnvVariable("GOOS", goos). + WithEnvVariable("GOARCH", goarch). + WithExec([]string{"go", "build", "-ldflags", ldflagsArgs, "-o", bin_path + "harbor", "/src/cmd/harbor/main.go"}). + WithWorkdir(bin_path). + WithExec([]string{"ls"}). + WithEntrypoint([]string{"./harbor"}) + + builds = append(builds, builder) + } + } + return builds +} + +// Sign signs a container image using Cosign, works also with GitHub Actions +func (m *HarborCli) Sign(ctx context.Context, + // +optional + githubToken *dagger.Secret, + // +optional + actionsIdTokenRequestUrl *dagger.Secret, + // +optional + actionsIdTokenRequestToken *dagger.Secret, + registryUsername string, + registryPassword *dagger.Secret, + imageAddr string, +) (string, error) { + registryPasswordPlain, _ := registryPassword.Plaintext(ctx) + + cosing_ctr := dag.Container().From("cgr.dev/chainguard/cosign") + + // If githubToken is provided, use it to sign the image + if githubToken != nil { + if actionsIdTokenRequestUrl == nil || actionsIdTokenRequestToken == nil { + return "", fmt.Errorf("actionsIdTokenRequestUrl (exist=%s) and actionsIdTokenRequestToken (exist=%t) must be provided when githubToken is provided", actionsIdTokenRequestUrl, actionsIdTokenRequestToken != nil) + } + fmt.Printf("Setting the ENV Vars GITHUB_TOKEN, ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN to sign with GitHub Token") + cosing_ctr = cosing_ctr.WithSecretVariable("GITHUB_TOKEN", githubToken). + WithSecretVariable("ACTIONS_ID_TOKEN_REQUEST_URL", actionsIdTokenRequestUrl). + WithSecretVariable("ACTIONS_ID_TOKEN_REQUEST_TOKEN", actionsIdTokenRequestToken) + } + + return cosing_ctr.WithSecretVariable("REGISTRY_PASSWORD", registryPassword). + WithExec([]string{"cosign", "env"}). + WithExec([]string{ + "cosign", "sign", "--yes", "--recursive", + "--registry-username", registryUsername, + "--registry-password", registryPasswordPlain, + imageAddr, + "--timeout", "1m", + }).Stdout(ctx) +} + +func getVersion(tags []string) string { + for _, tag := range tags { + if strings.HasPrefix(tag, "v") { + return tag + } + } + return "latest" +} diff --git a/.dagger/release.go b/.dagger/release.go new file mode 100644 index 000000000..2784657c7 --- /dev/null +++ b/.dagger/release.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + + "dagger/harbor-cli/internal/dagger" +) + +func (m *HarborCli) PublishRelease(ctx context.Context, + buildDir *dagger.Directory, + // +ignore=[".gitignore"] + // +defaultPath="." + source *dagger.Directory, + token *dagger.Secret, +) (string, error) { + if !m.IsInitialized { + err := m.init(ctx, source) + if err != nil { + return "", err + } + } + + bins, err := DistBinaries(ctx, dag, buildDir) + if err != nil { + return "", err + } + + cmd := []string{"gh", "release", "upload", "v" + m.AppVersion} + cmd = append(cmd, bins...) + cmd = append(cmd, "/dist/checksums.txt") + cmd = append(cmd, "--clobber") + + ctr := dag.Container(). + From("debian:bookworm-slim"). + WithMountedDirectory("/src", source). + WithMountedDirectory("/dist", buildDir). + WithSecretVariable("GH_TOKEN", token). + WithExec([]string{"apt-get", "update"}). + WithExec([]string{"apt-get", "install", "-y", "curl", "git"}). + WithExec([]string{"curl", "-fsSL", "https://cli.github.com/packages/githubcli-archive-keyring.gpg", "-o", "/usr/share/keyrings/githubcli-archive-keyring.gpg"}). + WithExec([]string{"sh", "-c", `echo "deb [arch=amd64 signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list`}). + WithExec([]string{"apt-get", "update"}). + WithExec([]string{"apt-get", "install", "-y", "gh"}) + + return ctr. + WithWorkdir("/src"). + // Creating Release + WithExec([]string{"gh", "release", "create", "v" + m.AppVersion, "--generate-notes"}). + WithExec(cmd). + Stdout(ctx) +} diff --git a/.dagger/test.go b/.dagger/test.go new file mode 100644 index 000000000..1360edb90 --- /dev/null +++ b/.dagger/test.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + + "dagger/harbor-cli/internal/dagger" +) + +// Executes Go tests +func (m *HarborCli) Test(ctx context.Context, source *dagger.Directory) (string, error) { + err := m.init(ctx, source) + if err != nil { + return "", err + } + + test := dag.Container(). + From("golang:"+m.GoVersion+"-alpine"). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithExec([]string{"go", "test", "-v", "./..."}) + return test.Stdout(ctx) +} + +// Executes Go tests and returns TestReport in json file +// TestReport executes Go tests and returns only the JSON report file +func (m *HarborCli) TestReport(ctx context.Context, source *dagger.Directory) (*dagger.File, error) { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + + reportName := "TestReport.json" + test := dag.Container(). + From("golang:"+m.GoVersion+"-alpine"). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithExec([]string{"gotestsum", "--jsonfile", reportName, "./..."}) + + return test.File(reportName), nil +} + +// Tests Coverage of code base +func (m *HarborCli) TestCoverage(ctx context.Context, source *dagger.Directory) (*dagger.File, error) { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + coverage := "coverage.out" + test := dag.Container(). + From("golang:"+m.GoVersion+"-alpine"). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithExec([]string{"go", "install", "gotest.tools/gotestsum@latest"}). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithExec([]string{"gotestsum", "--", "-coverprofile=" + coverage, "./..."}) + + return test.File(coverage), nil +} + +// TestCoverageReport processes coverage data and returns a formatted markdown report +func (m *HarborCli) TestCoverageReport(ctx context.Context, source *dagger.Directory) (*dagger.File, error) { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + coverageFile := "coverage.out" + reportFile := "coverage-report.md" + test := dag.Container(). + From("golang:"+m.GoVersion+"-alpine"). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src"). + WithExec([]string{"apk", "add", "--no-cache", "bc"}). + WithExec([]string{"go", "test", "-coverprofile=" + coverageFile, "./..."}) + return test.WithExec([]string{"sh", "-c", ` + echo "

πŸ“Š Test Coverage Results

" > ` + reportFile + ` + if [ ! -f "` + coverageFile + `" ]; then + echo "

❌ Coverage file not found!

" >> ` + reportFile + ` + exit 1 + fi + total_coverage=$(go tool cover -func=` + coverageFile + ` | grep total: | grep -Eo '[0-9]+\.[0-9]+') + echo "DEBUG: Total coverage is $total_coverage" >&2 + if (( $(echo "$total_coverage >= 80.0" | bc -l) )); then + emoji="βœ…" + elif (( $(echo "$total_coverage >= 60.0" | bc -l) )); then + emoji="⚠️" + else + emoji="❌" + fi + echo "

Total coverage: $emoji $total_coverage% (Target: 80%)

" >> ` + reportFile + ` + echo "
Detailed package coverage
" >> ` + reportFile + `
+        go tool cover -func=` + coverageFile + ` >> ` + reportFile + `
+        echo "
" >> ` + reportFile + ` + cat ` + reportFile + ` >&2 + `}).File(reportFile), nil +} diff --git a/.dagger/utils.go b/.dagger/utils.go new file mode 100644 index 000000000..cfa461af2 --- /dev/null +++ b/.dagger/utils.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "dagger/harbor-cli/internal/dagger" +) + +func DistBinaries(ctx context.Context, s *dagger.Client, dist *dagger.Directory) ([]string, error) { + dirs := []string{"archive", "deb", "rpm", "apk"} + var files []string + + ctr := s.Container(). + From("alpine:latest"). + WithMountedDirectory("/dist", dist). + WithWorkdir("/dist") + + for _, v := range dirs { + out, err := ctr.WithExec([]string{"ls", v}).Stdout(ctx) + if err != nil { + return nil, err + } + + bins := strings.Split(out, "\n") + for _, bin := range bins { + if bin != "" && bin != "nfpm.yml" { + files = append(files, filepath.Join("/", "dist", v, bin)) + } + } + } + + return files, nil +} + +func LDFlags(ctx context.Context, version, goVersion, buildTime, commit string) string { + return fmt.Sprintf("-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.Version=%s "+ + "-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GoVersion=%s "+ + "-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.BuildTime=%s "+ + "-X github.com/goharbor/harbor-cli/cmd/harbor/internal/version.GitCommit=%s", + version, goVersion, buildTime, commit, + ) +} diff --git a/.dagger/vuln.go b/.dagger/vuln.go new file mode 100644 index 000000000..c604375ff --- /dev/null +++ b/.dagger/vuln.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "fmt" + + "dagger/harbor-cli/internal/dagger" +) + +// Checks for vulnerabilities using govulncheck +func (m *HarborCli) vulnerabilityCheck(ctx context.Context) *dagger.Container { + return dag.Container(). + From("golang:"+m.GoVersion+"-alpine"). + WithMountedCache("/go/pkg/mod", dag.CacheVolume("go-mod-"+m.GoVersion)). + WithEnvVariable("GOMODCACHE", "/go/pkg/mod"). + WithMountedCache("/go/build-cache", dag.CacheVolume("go-build-"+m.GoVersion)). + WithEnvVariable("GOCACHE", "/go/build-cache"). + WithExec([]string{"go", "install", "golang.org/x/vuln/cmd/govulncheck@latest"}). + WithMountedDirectory("/src", m.Source). + WithWorkdir("/src") +} + +// Runs a vulnerability check using govulncheck +func (m *HarborCli) VulnerabilityCheck(ctx context.Context, source *dagger.Directory) (string, error) { + err := m.init(ctx, source) + if err != nil { + return "", err + } + return m.vulnerabilityCheck(ctx). + WithExec([]string{"govulncheck", "-show", "verbose", "./..."}). + Stderr(ctx) +} + +// Runs a vulnerability check using govulncheck and writes results to vulnerability-check.report +func (m *HarborCli) VulnerabilityCheckReport(ctx context.Context, source *dagger.Directory) (*dagger.File, error) { + err := m.init(ctx, source) + if err != nil { + return nil, err + } + + report := "vulnerability-check.report" + cmd := fmt.Sprintf("govulncheck ./... > %s || true", report) + + return m.vulnerabilityCheck(ctx). + WithExec([]string{ + "sh", "-c", cmd, + }).File(report), nil +} diff --git a/.github/actions/publish-and-sign/action.yaml b/.github/actions/publish-and-sign/action.yaml index 73a613fdd..e50c8dcd7 100644 --- a/.github/actions/publish-and-sign/action.yaml +++ b/.github/actions/publish-and-sign/action.yaml @@ -17,6 +17,10 @@ inputs: REGISTRY_USERNAME: description: 'Registry username' required: true + BUILD_DIR: + description: 'Build Directory path' + required: false + default: "" runs: using: "composite" @@ -27,14 +31,45 @@ runs: - name: Install Cosign uses: sigstore/cosign-installer@v3.7.0 + - name: Get Build Artifact + if: ${{ inputs.BUILD_DIR != '' }} + uses: actions/download-artifact@v4 + with: + name: build-dir + path: ${{ inputs.BUILD_DIR }} + - name: Check Env Variables shell: bash env: GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }} run: cosign env + + - name: Publish and Sign Snapshot Image (Build Dir not present) + uses: dagger/dagger-for-github@v7 + if: ${{ inputs.BUILD_DIR == '' }} + env: + GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }} + REGISTRY_ADDRESS: ${{ inputs.REGISTRY_ADDRESS }} + REGISTRY_USERNAME: ${{ inputs.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ inputs.REGISTRY_PASSWORD }} + IMAGE_TAGS: ${{ inputs.IMAGE_TAGS }} + with: + version: ${{ steps.dagger_version.outputs.version }} + verb: call + args: | + publish-image-and-sign \ + --progress=plain \ + --registry=${{ env.REGISTRY_ADDRESS }} \ + --registry-username=${{ env.REGISTRY_USERNAME }} \ + --registry-password=env://REGISTRY_PASSWORD \ + --image-tags=${{ env.IMAGE_TAGS }} \ + --github-token=env://GITHUB_TOKEN \ + --actions-id-token-request-url=env://ACTIONS_ID_TOKEN_REQUEST_URL \ + --actions-id-token-request-token=env://ACTIONS_ID_TOKEN_REQUEST_TOKEN \ - - name: Publish and Sign Snapshot Image + - name: Publish and Sign Snapshot Image (Build Dir present) uses: dagger/dagger-for-github@v7 + if: ${{ inputs.BUILD_DIR != '' }} env: GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }} REGISTRY_ADDRESS: ${{ inputs.REGISTRY_ADDRESS }} @@ -44,11 +79,14 @@ runs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: "publish-image-and-sign \ - --registry='${{ env.REGISTRY_ADDRESS }}' \ - --registry-username='${{ env.REGISTRY_USERNAME }}' \ - --registry-password=env:REGISTRY_PASSWORD \ - --image-tags='${{ env.IMAGE_TAGS}}' \ - --github-token=env:GITHUB_TOKEN \ - --actions-id-token-request-url=$ACTIONS_ID_TOKEN_REQUEST_URL \ - --actions-id-token-request-token=env:ACTIONS_ID_TOKEN_REQUEST_TOKEN" + args: | + publish-image-and-sign \ + --progress=plain \ + --registry=${{ env.REGISTRY_ADDRESS }} \ + --registry-username=${{ env.REGISTRY_USERNAME }} \ + --registry-password=env://REGISTRY_PASSWORD \ + --image-tags=${{ env.IMAGE_TAGS }} \ + --github-token=env://GITHUB_TOKEN \ + --actions-id-token-request-url=env://ACTIONS_ID_TOKEN_REQUEST_URL \ + --actions-id-token-request-token=env://ACTIONS_ID_TOKEN_REQUEST_TOKEN \ + --build-dir=${{ inputs.BUILD_DIR }} \ diff --git a/.github/workflows/default.yaml b/.github/workflows/default.yaml index 67c23c96e..21ae9d89a 100644 --- a/.github/workflows/default.yaml +++ b/.github/workflows/default.yaml @@ -10,6 +10,9 @@ on: - "*.md" - "assets/**" +permissions: + contents: write + jobs: lint: runs-on: ubuntu-latest @@ -27,7 +30,7 @@ jobs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: run-doc export --path=doc + args: run-doc --source=. export --path=doc - name: Check for changes run: | @@ -50,7 +53,7 @@ jobs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: lint-report export --path=golangci-lint.report + args: lint-report --source=. export --path=golangci-lint.report - name: Generate lint summary run: | @@ -63,13 +66,6 @@ jobs: exit 1 fi - # - uses: reviewdog/action-setup@v1 - # - name: Run Reviewdog - # env: - # REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # run: | - # reviewdog -f=sarif -name="Golang Linter Report" -reporter=github-check -filter-mode nofilter -fail-level any -tee < golangci-lint-report.sarif - vulnerability-check: runs-on: ubuntu-latest steps: @@ -86,7 +82,7 @@ jobs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: vulnerability-check-report export --path=vulnerability-check.report + args: vulnerability-check-report --source=. export --path=vulnerability-check.report - name: Generate vulnerability summary run: | @@ -99,24 +95,6 @@ jobs: exit 1 fi - test-release: - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Dagger Version - uses: sagikazarmark/dagger-version-action@v0.0.1 - - - name: Test Release - uses: dagger/dagger-for-github@v7 - with: - version: ${{ steps.dagger_version.outputs.version }} - verb: call - args: snapshot-release - test-code: runs-on: ubuntu-latest steps: @@ -130,7 +108,7 @@ jobs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: test-report export --path=TestReport.json + args: test-report --source=. export --path=TestReport.json - name: Summarize Tests uses: robherley/go-test-action@v0.6.0 @@ -143,7 +121,7 @@ jobs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: test-coverage-report export --path=coverage-report.md + args: test-coverage-report --source=. export --path=coverage-report.md - name: Add coverage to step summary if: github.event_name == 'pull_request' @@ -155,7 +133,7 @@ jobs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: test-coverage export --path=coverage.out + args: test-coverage --source=. export --path=coverage.out - uses: codecov/codecov-action@v5 if: github.event_name == 'pull_request' @@ -169,7 +147,7 @@ jobs: with: version: ${{ steps.dagger_version.outputs.version }} verb: call - args: build-dev --platform linux/amd64 export --path=./harbor-dev + args: build-dev --source=. --platform linux/amd64 export --path=./harbor-dev push-latest-images: needs: @@ -224,17 +202,61 @@ jobs: with: fetch-depth: 0 - - name: Push images + - name: Create Build Dir if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) - uses: ./.github/actions/publish-and-sign + run: mkdir -p dist + + - name: Building Binaries + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) + uses: dagger/dagger-for-github@v7 + with: + version: "latest" + verb: call + args: "build --build-dir=./dist export --path=./dist" + + - name: Archiving Binaries + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) + uses: dagger/dagger-for-github@v7 + with: + version: "latest" + verb: call + args: "archive --build-dir=./dist export --path=./dist" + + - name: NFPM Build (deb/rpm) + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) + uses: dagger/dagger-for-github@v7 + with: + version: "latest" + verb: call + args: "nfpm-build --build-dir=./dist export --path=./dist" + + - name: APK Build (.apk) + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) + uses: dagger/dagger-for-github@v7 + with: + version: "latest" + verb: call + args: "apk --build-dir=./dist export --path=./dist" + + - name: Creating Checksum + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) + uses: dagger/dagger-for-github@v7 with: - IMAGE_TAGS: latest, ${{ github.ref_name }} + version: "latest" + verb: call + args: "checksum --build-dir=./dist export --path=./dist" + + - name: Publish Release + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) + uses: dagger/dagger-for-github@v7 + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - REGISTRY_ADDRESS: ${{ vars.REGISTRY_ADDRESS }} - REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }} + with: + version: "latest" + verb: call + args: "publish-release --build-dir=./dist --token=env://GITHUB_TOKEN " - - name: Create Release + - name: Apt Build if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) uses: dagger/dagger-for-github@v7 env: @@ -242,14 +264,21 @@ jobs: with: version: "latest" verb: call - args: "release --github-token=env:GITHUB_TOKEN" + args: "apt-build --build-dir=./dist --token=env://GITHUB_TOKEN " + + - name: Upload Build Artifact + uses: actions/upload-artifact@v4 + with: + name: build-dir + path: ./dist - name: Publish and Sign Tagged Image if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/')) uses: ./.github/actions/publish-and-sign with: - IMAGE_TAGS: "latest, ${{ github.ref_name }}" + IMAGE_TAGS: "latest,${{ github.ref_name }}" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} REGISTRY_ADDRESS: ${{ vars.REGISTRY_ADDRESS }} REGISTRY_USERNAME: ${{ vars.REGISTRY_USERNAME }} + BUILD_DIR: "dist" diff --git a/dagger.json b/dagger.json index f1b2f6d1e..7929c9f18 100644 --- a/dagger.json +++ b/dagger.json @@ -1,6 +1,6 @@ { "name": "harbor-cli", - "engineVersion": "v0.18.12", + "engineVersion": "v0.19.7", "sdk": { "source": "go" }, @@ -11,5 +11,6 @@ "pin": "8359122a7b90f2c8c6f3165570fdcbec6e923023" } ], - "source": ".dagger" + "source": ".dagger", + "disableDefaultFunctionCaching": true }