Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pkg/gitprovider/ — Per-provider adapters: github.go, gitlab.go, bitb
Factory: NewProvider() dispatches on GitServer.Spec.GitProvider
pkg/client/jira/ — Jira REST client (go-jira wrapper + mockable Client interface)
pkg/gerrit/ — SSH-based Gerrit client
pkg/git/ — go-git wrapper + factory (supports SSH/HTTPS auth)
pkg/git/ — go-git wrapper + factory (supports SSH/HTTPS auth); Clone and Push run the git binary (cli.go) unless GIT_CLI_ENABLED=false
pkg/webhook/ — Admission webhooks: Codebase and CodebaseBranch validation
pkg/objectmodifier/ — Applies default values / label patches before reconcile logic
pkg/util/ — Shared helpers (workdir, timeouts, URL utilities, templates)
Expand Down Expand Up @@ -100,6 +100,7 @@ Deletion runs a shorter chain: `DeleteWebHook` → `Cleaner`.
- `WATCH_NAMESPACE` — namespace the operator watches (required)
- `CODEBASE_BRANCH_MAX_CONCURRENT_RECONCILES` — concurrency for CodebaseBranch reconciler (default 1)
- `ENABLE_WEBHOOKS` — set to `false` to disable admission webhooks
- `GIT_CLI_ENABLED` — set to `false` to clone and push with go-git instead of the git binary
- `TELEMETRY_ENABLED` / `TELEMETRY_DELAY` — opt-in usage telemetry

### Adding a new chain handler
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM alpine:3.18.12
FROM alpine:3.24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question (non-blocking): this PR contains two changes: the base image fix (Alpine 3.18 -> 3.24, which unblocks every build, including #325) and switching clone/push from go-git to the git binary (~1.6k lines, mostly new code and tests).

Was there a reason to keep them together? Splitting them could let the build fix be reviewed and merged quickly, while the git CLI change gets a more focused review. If they depend on each other (e.g. the image needs git/ssh only because of the new code), that makes sense, I just wanted to ask.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks! This is intentional: the PO asked to bring the git CLI back in this PR, because go-git runs out of memory on large repositories.


ARG TARGETARCH

Expand All @@ -9,9 +9,9 @@ ENV ASSETS_DIR=/usr/local/bin \
USER_NAME=codebase-operator \
USER_UID=1001

RUN apk add --no-cache ca-certificates=20241121-r1 \
openssh-client==9.3_p2-r3 \
git==2.40.4-r0
# pkg/git/cli.go runs git and ssh. The ~= pins match the minors the FROM image ships: bump them with the FROM tag.
RUN apk upgrade --no-cache && \
apk add --no-cache git~=2.54 openssh-client~=10.3

RUN adduser -h ${HOME} -s /bin/ash -D -u ${USER_UID} codebase-operator

Expand Down
36 changes: 36 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"crypto/tls"
"flag"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"time"
Expand Down Expand Up @@ -393,6 +395,8 @@ func main() {
setupLog.Info("Stale branch checker is disabled", "env", branchStaleCheckIntervalEnv)
}

checkGitEngine()

setupLog.Info("starting manager")

ctx := ctrl.SetupSignalHandler()
Expand All @@ -408,6 +412,38 @@ func main() {
}
}

// checkGitEngine logs what clones and pushes repositories and exits when it is the git binary and git is missing.
func checkGitEngine() {
if value, ok := os.LookupEnv(gitproviderv2.CLIEnabledEnv); ok {
if _, err := strconv.ParseBool(value); err != nil {
setupLog.Error(err, "Invalid value, clone and push keep the git binary", "env", gitproviderv2.CLIEnabledEnv)
}
}

if !gitproviderv2.CLIEnabled() {
setupLog.Info("Clone and push run go-git", "env", gitproviderv2.CLIEnabledEnv)
return
}

if _, err := exec.LookPath("git"); err != nil {
setupLog.Error(err, "Clone and push need the git binary; set env to false to use go-git",
"env", gitproviderv2.CLIEnabledEnv)
os.Exit(1)
}

if _, err := exec.LookPath("ssh"); err != nil {
setupLog.Error(err, "Clone and push over SSH need the ssh binary", "env", gitproviderv2.CLIEnabledEnv)
}

// ssh exits when its UID has no passwd entry; the image's entrypoint adds one for a process in group 0.
if _, err := user.LookupId(strconv.Itoa(os.Getuid())); err != nil {
setupLog.Error(err, "Clone and push over SSH will fail: run the pod with group 0, or set env to false",
"env", gitproviderv2.CLIEnabledEnv)
}

setupLog.Info("Clone and push run the git binary", "env", gitproviderv2.CLIEnabledEnv)
}

func getMaxConcurrentReconciles(envVar string) int {
val, exists := os.LookupEnv(envVar)
if !exists {
Expand Down
4 changes: 2 additions & 2 deletions deploy-templates/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ annotations:
artifacthub.io/images: |
- name: codebase-operator:2.36.0
image: epamedp/codebase-operator:2.36.0
- name: alpine:3.18.12
image: alpine:3.18.12
- name: alpine:3.24
image: alpine:3.24
artifacthub.io/operatorCapabilities: Full Lifecycle
artifacthub.io/crds: |
- kind: CDStageDeploy
Expand Down
1 change: 1 addition & 0 deletions deploy-templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ A Helm chart for KubeRocketCI Codebase Operator
| envs[0].name | string | `"RECONCILATION_PERIOD"` | |
| envs[0].value | string | `"360"` | |
| envs[1] | object | `{"name":"CODEBASE_BRANCH_MAX_CONCURRENT_RECONCILES","value":"3"}` | Maximum number of parallel reconciliation codebasebranches |
| gitCliEnabled | bool | `true` | Clone and push repositories with the git binary. Over SSH the pod's UID needs a passwd entry, which the image adds for any UID in group 0, as OpenShift assigns. Set to false to use go-git, which clones and pushes inside the operator's process: onboarding a large repository can then exceed the memory limit. |
| global.gatewayApi | object | `{"gatewayName":"main-gateway","gatewayNamespace":"envoy-gateway-system"}` | Gateway API parent Gateway that EventListener HTTPRoutes attach to (used when ingressController=envoy) |
| global.gatewayApi.gatewayName | string | `"main-gateway"` | Name of the parent Gateway resource |
| global.gatewayApi.gatewayNamespace | string | `"envoy-gateway-system"` | Namespace of the parent Gateway resource |
Expand Down
2 changes: 2 additions & 0 deletions deploy-templates/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ spec:
value: {{ .Values.enableWebhooks | quote }}
- name: BRANCH_STALE_CHECK_INTERVAL
value: {{ .Values.branchStaleCheckInterval | quote }}
- name: GIT_CLI_ENABLED
value: {{ .Values.gitCliEnabled | quote }}
- name: SSH_KNOWN_HOSTS
value: /etc/codebase-operator/ssh/ssh_known_hosts
{{- if .Values.caCerts.enabled }}
Expand Down
6 changes: 6 additions & 0 deletions deploy-templates/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ enableWebhooks: true
# Accepts Go duration strings (e.g. 24h, 30m); "0" disables the check.
branchStaleCheckInterval: 24h

# -- Clone and push repositories with the git binary. Over SSH the pod's UID needs a passwd
# entry, which the image adds for any UID in group 0, as OpenShift assigns. Set to false to
# use go-git, which clones and pushes inside the operator's process: onboarding a large
# repository can then exceed the memory limit.
gitCliEnabled: true

# SSH host key verification. Every SSH connection the operator makes is verified
# against these entries; there is no way to disable verification. GitServers that
# authenticate with a token over HTTPS are unaffected.
Expand Down
41 changes: 40 additions & 1 deletion pkg/git/checkout_remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func workdirHead(t *testing.T, workDir string) *plumbing.Reference {
}

// A branch pushed to origin after the workdir was cloned is fetched into
// refs/heads by Checkout's refs/*:refs/* refspec; the existence check must look
// refs/heads by Checkout's refs/heads/* refspec; the existence check must look
// there, or the checkout collides on Create with the ref the fetch just wrote.
func TestCheckout_RemoteBranchCreatedAfterClone(t *testing.T) {
originDir, workDir := buildOriginWithClone(t)
Expand Down Expand Up @@ -123,6 +123,45 @@ func TestCheckout_BranchAbsentEverywhere(t *testing.T) {
require.Equal(t, "brand-new", workdirHead(t, workDir).Name().Short())
}

// The checkout fetches take branches and tags only: a pull request ref, which
// refs/* would match, stays on the remote.
func TestCheckout_FetchesOnlyBranchesAndTags(t *testing.T) {
tests := []struct {
name string
checkout func(ctx context.Context, gp *GitProvider, workDir string) error
}{
{
name: "Checkout with remote",
checkout: func(ctx context.Context, gp *GitProvider, workDir string) error {
return gp.Checkout(ctx, workDir, "late-branch", true)
},
},
{
name: "CheckoutRemoteBranch",
checkout: func(ctx context.Context, gp *GitProvider, workDir string) error {
return gp.CheckoutRemoteBranch(ctx, workDir, "late-branch")
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originDir, workDir := buildOriginWithClone(t)
seedCommit(t, originDir, "pull-request", "+refs/heads/master:refs/pull/1/head")
seedCommit(t, originDir, "late", "+refs/heads/master:refs/heads/late-branch")

require.NoError(t, tt.checkout(t.Context(), NewGitProvider(Config{}), workDir))
require.Equal(t, "late-branch", workdirHead(t, workDir).Name().Short())

repo, err := gogit.PlainOpen(workDir)
require.NoError(t, err)

_, err = repo.Reference("refs/pull/1/head", false)
require.ErrorIs(t, err, plumbing.ErrReferenceNotFound)
})
}
}

// TestCheckout_ForcePushedRemoteBranch: a rewritten upstream branch must not
// fail the fetch of a cached workdir; the checkout lands on the new tip.
func TestCheckout_ForcePushedRemoteBranch(t *testing.T) {
Expand Down
Loading
Loading