mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
* feat(api): hard-fork CRD API group argoproj.io -> deploy.hanzo.ai
Hanzo CD hard-fork, phase 1.1. Single source of truth is
pkg/apis/application/register.go's Group constant (Application/AppProject/
ApplicationSet all derive their FullName from it); every other Go call-site
that hardcoded the literal group string is updated to match: the 3 generated
informer GVRs, the app-of-apps nested-Application detector in types.go, the
resource-tree Application-kind cache populator, the 3 NotFound error
constructors in server/application, and the applicationset-controller's
leader-election Lease name/ID (kept in lockstep across Go source + RBAC
resourceNames, or leader election would get RBAC-denied on startup).
CRD YAMLs (application/appproject/applicationset) and every RBAC apiGroups
rule that actually governs our 3 CRDs are repointed to deploy.hanzo.ai, across
both manifests/base/* (namespaced Roles) and manifests/cluster-rbac/* (cluster
scope). Two adjacent things that share the "argoproj.io" string but are NOT
ours are deliberately left untouched: gitops-engine's health-check dispatch
for Argo Workflows' `Workflow` kind, and argocd-server-clusterrole's second
apiGroups block granting read on Argo Workflows' `workflows` resource — both
belong to a separate, unrelated Argoproj-family project we are not forking.
Also untouched, per explicit scope: annotation/label keys (argocd.argoproj.io/*)
and finalizers (*.argoproj.io) that track already-applied live resources, and
argocd-cm.yaml's resource.customizations.ignoreResourceUpdates keys (a narrow
app-of-apps diffing nicety, not build- or RBAC-relevant).
CGO_ENABLED=0 go build ./... verified green.
* ci: wire canonical hanzo.yml + cicd.yml (Hanzo CD image build)
Phase 1.3. Follows the same one-way pattern every hanzo/lux/zoo repo uses:
hanzo.yml declares the image (root Dockerfile -> ghcr.io/hanzoai/deploy) and a
fast go-build sanity test (the multi-stage Dockerfile build is the slow, real
gate); cicd.yml is the ~10-line trigger shim importing hanzoai/ci's reusable
build workflow on the arc pool, secrets from KMS only.
No `deploy:` block yet, matching the hanzoai/cms build-only-first-cutover
pattern: this repo's own live hanzo-cd namespace runs the whole Hanzo fleet's
GitOps, so the prod image/CRD swap is a deliberate separate action (see the
[HELD] universe PR), never an automatic rollout on merge. Every PR/push still
builds and pushes an immutable ghcr.io/hanzoai/deploy:sha-<7>-amd64 image.
* feat(ui): rebrand Argo CD -> Hanzo CD (title, favicon, logo, sidebar)
Phase 1.2. User-facing "Argo CD"/"ArgoCD" text replaced with "Hanzo CD"
across the login page, help modal, settings screens, version-info panel,
sync-options warning, fullscreen-logs title, app.tsx page titles, and
index.html's <title>/noscript text (dropped the now-wrong upstream CLI
doc link rather than invent a hanzo.ai URL that doesn't exist).
Logo/favicon assets replaced with the canonical Hanzo mark (never
hand-drawn, copied from ~/work/hanzo/logo/dist): the two real usages of
Argo's logo collapse onto one Hanzo glyph (mono + white variants) at the
same 5 call sites (login icon + footer, login.scss $logoPath/
$logoNegativePath, sidebar text-logo), preserving the existing CSS
invert() filters so both the light login panel and dark sidebar still
render correctly. Old argologo.svg/argo_o.svg/argo.png deleted (no
longer referenced). favicon-{16x16,32x32}.png added at
ui/src/assets/favicon/ (index.html's links to that path were previously
dead - the directory didn't exist in this checkout). package.json name
argo-cd-ui -> hanzo-cd-ui.
Left alone, deliberately: the two Kubernetes resource-kind icon sets
under ui/src/assets/images/resources/ (unrelated to branding), and
upstream doc/project links (argoproj.io, argo-cd.readthedocs.io) since
we have no equivalent hanzo.ai docs to point to yet - except the login
footer logo's outbound link, which now points to hanzo.ai instead of
argoproj.io (a Hanzo-branded mark linking to the upstream project it
was forked from read as an obvious leftover).
* feat(module): rename Go module github.com/argoproj/argo-cd/v3 -> github.com/hanzoai/deploy
Phase 2. No version suffix on the new path (org policy: never publish our
own Go modules above v1.x.x / with a v2+ suffix) — this fork starts a fresh
v0/v1 module identity rather than carrying the v3 the upstream fork point
had reached.
Mechanical, symmetric rewrite: 704 .go files (import statements only; two
disjoint substitutions applied specific-first so the nested gitops-engine
module path never double-matches), plus go.mod (module line, the
gitops-engine require+replace pair, version reset to v0.0.0-... since a
suffix-less path can't carry a v3 semver), gitops-engine/go.mod, Makefile
(ldflags PACKAGE + 2 TEST_MODULE filters), .golangci.yaml, .goreleaser.yaml,
.mockery.yaml (+ gitops-engine's), Procfile, the codegen/proto generation
scripts (hack/generate-proto.sh, update-codegen.sh, update-openapi.sh), and
20 .proto files' go_package options. Also one OpenTelemetry tracer name
string in gitops-engine/pkg/sync/sync_context.go (a cosmestic label, not an
import, but worth keeping consistent).
CGO_ENABLED=0 go build ./... verified green for both the root module and
the nested gitops-engine module; go vet clean on both; go mod tidy run
(network available) to reconcile go.sum, dependency set unchanged.
Left alone, deliberately: doc-only mentions (docs/snyk/*.html scan reports,
gitops-engine/README.md), a comment-only reference in
.github/workflows/release.yaml, and one Renovate bot config line — none are
build- or codegen-load-bearing. Also left alone: comments citing upstream
GitHub issue URLs (github.com/argoproj/argo-cd/issues/NNNNN) and a handful
of test fixtures using "https://github.com/argoproj/argo-cd.git" as a
representative example repo URL — both are accurate historical/test-data
references, not stale self-references.
---------
Co-authored-by: Hanzo Dev <[email protected]>
907 lines
28 KiB
Go
907 lines
28 KiB
Go
package git
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hanzoai/deploy/common"
|
|
"github.com/hanzoai/deploy/test/fixture/log"
|
|
"github.com/hanzoai/deploy/test/fixture/path"
|
|
"github.com/hanzoai/deploy/test/fixture/test"
|
|
"github.com/hanzoai/deploy/util/proxy"
|
|
)
|
|
|
|
func setupGitEnv(t *testing.T) {
|
|
t.Helper()
|
|
cwd, err := os.Getwd()
|
|
require.NoError(t, err)
|
|
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
|
|
t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(cwd, "testdata", "gitconfig"))
|
|
}
|
|
|
|
func TestIsCommitSHA(t *testing.T) {
|
|
t.Parallel()
|
|
assert.True(t, IsCommitSHA("9d921f65f3c5373b682e2eb4b37afba6592e8f8b"))
|
|
assert.True(t, IsCommitSHA("9D921F65F3C5373B682E2EB4B37AFBA6592E8F8B"))
|
|
assert.False(t, IsCommitSHA("gd921f65f3c5373b682e2eb4b37afba6592e8f8b"))
|
|
assert.False(t, IsCommitSHA("master"))
|
|
assert.False(t, IsCommitSHA("HEAD"))
|
|
assert.False(t, IsCommitSHA("9d921f6")) // only consider 40 characters hex strings as a commit-sha
|
|
assert.True(t, IsTruncatedCommitSHA("9d921f6"))
|
|
assert.False(t, IsTruncatedCommitSHA("9d921f")) // we only consider 7+ characters
|
|
assert.False(t, IsTruncatedCommitSHA("branch-name"))
|
|
}
|
|
|
|
func TestEnsurePrefix(t *testing.T) {
|
|
t.Parallel()
|
|
data := [][]string{
|
|
{"world", "hello", "helloworld"},
|
|
{"helloworld", "hello", "helloworld"},
|
|
{"example.com", "https://", "https://example.com"},
|
|
{"https://example.com", "https://", "https://example.com"},
|
|
{"cd", "argo", "argocd"},
|
|
{"argocd", "argo", "argocd"},
|
|
{"", "argocd", "argocd"},
|
|
{"argocd", "", "argocd"},
|
|
}
|
|
for _, table := range data {
|
|
result := ensurePrefix(table[0], table[1])
|
|
assert.Equal(t, table[2], result)
|
|
}
|
|
}
|
|
|
|
func TestIsSSHURL(t *testing.T) {
|
|
t.Parallel()
|
|
data := map[string]bool{
|
|
"git://github.com/argoproj/test.git": false,
|
|
"[email protected]:argoproj/test.git": true,
|
|
"[email protected]:test": true,
|
|
"[email protected]:test.git": true,
|
|
"https://github.com/argoproj/test": false,
|
|
"https://github.com/argoproj/test.git": false,
|
|
"ssh://[email protected]:argoproj/test": true,
|
|
"ssh://[email protected]:argoproj/test.git": true,
|
|
"ssh://[email protected]:test.git": true,
|
|
}
|
|
for k, v := range data {
|
|
isSSH, _ := IsSSHURL(k)
|
|
assert.Equal(t, v, isSSH)
|
|
}
|
|
}
|
|
|
|
func TestIsSSHURLUserName(t *testing.T) {
|
|
t.Parallel()
|
|
isSSH, user := IsSSHURL("ssh://[email protected]:29418/project")
|
|
assert.True(t, isSSH)
|
|
assert.Equal(t, "john", user)
|
|
|
|
isSSH, user = IsSSHURL("[email protected]:29418/project")
|
|
assert.True(t, isSSH)
|
|
assert.Equal(t, "john", user)
|
|
|
|
isSSH, user = IsSSHURL("[email protected]@john-server.org:29418/project")
|
|
assert.True(t, isSSH)
|
|
assert.Equal(t, "[email protected]", user)
|
|
|
|
isSSH, user = IsSSHURL("ssh://[email protected]@john-server.org:29418/project")
|
|
assert.True(t, isSSH)
|
|
assert.Equal(t, "[email protected]", user)
|
|
|
|
isSSH, user = IsSSHURL("[email protected]@john-server.org:project")
|
|
assert.True(t, isSSH)
|
|
assert.Equal(t, "[email protected]", user)
|
|
|
|
isSSH, user = IsSSHURL("[email protected]@john-server.org:29418/project")
|
|
assert.True(t, isSSH)
|
|
assert.Equal(t, "[email protected]", user)
|
|
}
|
|
|
|
func TestSSHHostWithPort(t *testing.T) {
|
|
t.Parallel()
|
|
data := map[string]string{
|
|
"[email protected]:argoproj/test.git": "github.com:22",
|
|
"ssh://[email protected]/argoproj/test.git": "github.com:22",
|
|
"ssh://[email protected]:2222/argoproj/test.git": "github.com:2222",
|
|
"ssh://[email protected]:29418/project": "john-server.org:29418",
|
|
"https://github.com/argoproj/test": "",
|
|
"": "",
|
|
}
|
|
for repoURL, want := range data {
|
|
assert.Equal(t, want, SSHHostWithPort(repoURL), "input: %q", repoURL)
|
|
}
|
|
}
|
|
|
|
func TestSameURL(t *testing.T) {
|
|
t.Parallel()
|
|
data := map[string]string{
|
|
"[email protected]:argoproj/test": "[email protected]:argoproj/test.git",
|
|
"[email protected]:argoproj/test.git": "[email protected]:argoproj/test.git",
|
|
"[email protected]:test": "[email protected]:test.git",
|
|
"[email protected]:test.git": "[email protected]:test.git",
|
|
"https://GITHUB.com/argoproj/test": "https://github.com/argoproj/test.git",
|
|
"https://GITHUB.com/argoproj/test.git": "https://github.com/argoproj/test.git",
|
|
"https://github.com/FOO": "https://github.com/foo",
|
|
"https://github.com/TEST": "https://github.com/TEST.git",
|
|
"https://github.com/TEST.git": "https://github.com/TEST.git",
|
|
"https://github.com:4443/TEST": "https://github.com:4443/TEST.git",
|
|
"https://github.com:4443/TEST.git": "https://github.com:4443/TEST",
|
|
"ssh://[email protected]/argoproj/test": "[email protected]:argoproj/test.git",
|
|
"ssh://[email protected]/argoproj/test.git": "[email protected]:argoproj/test.git",
|
|
"ssh://[email protected]/test.git": "[email protected]:test.git",
|
|
"ssh://[email protected]/test": "[email protected]:test.git",
|
|
" https://github.com/argoproj/test ": "https://github.com/argoproj/test.git", //nolint:gocritic // This includes whitespaces for testing
|
|
"\thttps://github.com/argoproj/test\n": "https://github.com/argoproj/test.git",
|
|
"https://1234.visualstudio.com/myproj/_git/myrepo": "https://1234.visualstudio.com/myproj/_git/myrepo",
|
|
"https://dev.azure.com/1234/myproj/_git/myrepo": "https://dev.azure.com/1234/myproj/_git/myrepo",
|
|
}
|
|
for k, v := range data {
|
|
assert.True(t, SameURL(k, v))
|
|
}
|
|
}
|
|
|
|
func TestSanitizeRepoURL(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{"no credentials", "https://github.com/argoproj/argo-cd.git", "https://github.com/argoproj/argo-cd.git"},
|
|
{"strips user and password", "https://user:p@[email protected]/argoproj/argo-cd.git", "https://github.com/argoproj/argo-cd.git"},
|
|
{"strips token-only userinfo", "https://[email protected]/org/repo", "https://github.com/org/repo"},
|
|
{"scp-style ssh left unchanged", "[email protected]:argoproj/argo-cd.git", "[email protected]:argoproj/argo-cd.git"},
|
|
{"unparseable returned unchanged", "://not a url", "://not a url"},
|
|
{"empty returned unchanged", "", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
assert.Equal(t, tt.want, SanitizeRepoURL(tt.in))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCustomHTTPClient(t *testing.T) {
|
|
proxy.UseTestingProxyCallback()
|
|
|
|
certFile, err := filepath.Abs("../../test/fixture/certs/argocd-test-client.crt")
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, certFile)
|
|
|
|
keyFile, err := filepath.Abs("../../test/fixture/certs/argocd-test-client.key")
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, keyFile)
|
|
|
|
certData, err := os.ReadFile(certFile)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, string(certData))
|
|
|
|
keyData, err := os.ReadFile(keyFile)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, string(keyData))
|
|
|
|
// Get HTTPSCreds with client cert creds specified, and insecure connection
|
|
creds := NewHTTPSCreds("test", "test", "", string(certData), string(keyData), false, &NoopCredsStore{}, false)
|
|
client := GetRepoHTTPClient("https://localhost:9443/foo/bar", false, creds, "http://proxy:5000", "")
|
|
assert.NotNil(t, client)
|
|
assert.NotNil(t, client.Transport)
|
|
if client.Transport != nil {
|
|
transport := client.Transport.(*http.Transport)
|
|
assert.NotNil(t, transport.TLSClientConfig)
|
|
assert.True(t, transport.DisableKeepAlives)
|
|
assert.False(t, transport.TLSClientConfig.InsecureSkipVerify)
|
|
assert.NotNil(t, transport.TLSClientConfig.GetClientCertificate)
|
|
assert.Nil(t, transport.TLSClientConfig.RootCAs)
|
|
if transport.TLSClientConfig.GetClientCertificate != nil {
|
|
cert, err := transport.TLSClientConfig.GetClientCertificate(nil)
|
|
require.NoError(t, err)
|
|
if err == nil {
|
|
assert.NotNil(t, cert)
|
|
assert.NotEmpty(t, cert.Certificate)
|
|
assert.NotNil(t, cert.PrivateKey)
|
|
}
|
|
}
|
|
proxy, err := transport.Proxy(nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, proxy) // nil would mean no proxy is used
|
|
assert.Equal(t, "http://proxy:5000", proxy.String())
|
|
}
|
|
|
|
t.Setenv("http_proxy", "http://proxy-from-env:7878")
|
|
|
|
// Get HTTPSCreds without client cert creds, but insecure connection
|
|
creds = NewHTTPSCreds("test", "test", "", "", "", true, &NoopCredsStore{}, false)
|
|
client = GetRepoHTTPClient("https://localhost:9443/foo/bar", true, creds, "", "")
|
|
assert.NotNil(t, client)
|
|
assert.NotNil(t, client.Transport)
|
|
if client.Transport != nil {
|
|
transport := client.Transport.(*http.Transport)
|
|
assert.NotNil(t, transport.TLSClientConfig)
|
|
assert.True(t, transport.DisableKeepAlives)
|
|
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
|
|
assert.NotNil(t, transport.TLSClientConfig.GetClientCertificate)
|
|
assert.Nil(t, transport.TLSClientConfig.RootCAs)
|
|
if transport.TLSClientConfig.GetClientCertificate != nil {
|
|
cert, err := transport.TLSClientConfig.GetClientCertificate(nil)
|
|
require.NoError(t, err)
|
|
if err == nil {
|
|
assert.NotNil(t, cert)
|
|
assert.Empty(t, cert.Certificate)
|
|
assert.Nil(t, cert.PrivateKey)
|
|
}
|
|
}
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://proxy-from-env:7878", http.NoBody)
|
|
require.NoError(t, err)
|
|
proxy, err := transport.Proxy(req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "http://proxy-from-env:7878", proxy.String())
|
|
}
|
|
// GetRepoHTTPClient with root ca
|
|
cert, err := os.ReadFile("../../test/fixture/certs/argocd-test-server.crt")
|
|
require.NoError(t, err)
|
|
temppath := t.TempDir()
|
|
defer os.RemoveAll(temppath)
|
|
err = os.WriteFile(filepath.Join(temppath, "127.0.0.1"), cert, 0o666)
|
|
require.NoError(t, err)
|
|
t.Setenv(common.EnvVarTLSDataPath, temppath)
|
|
client = GetRepoHTTPClient("https://127.0.0.1", false, creds, "", "")
|
|
assert.NotNil(t, client)
|
|
assert.NotNil(t, client.Transport)
|
|
if client.Transport != nil {
|
|
transport := client.Transport.(*http.Transport)
|
|
assert.NotNil(t, transport.TLSClientConfig)
|
|
assert.True(t, transport.DisableKeepAlives)
|
|
assert.False(t, transport.TLSClientConfig.InsecureSkipVerify)
|
|
assert.NotNil(t, transport.TLSClientConfig.RootCAs)
|
|
}
|
|
}
|
|
|
|
func TestLsRemote(t *testing.T) {
|
|
setupGitEnv(t)
|
|
clnt, err := NewClientExt("https://github.com/argoproj/argo-cd.git", "/tmp", NopCreds{}, false, false, "", "")
|
|
require.NoError(t, err)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
revision string
|
|
expectedCommit string
|
|
}{
|
|
{
|
|
name: "should resolve symbolic link reference",
|
|
revision: "HEAD",
|
|
},
|
|
{
|
|
name: "should resolve branch name",
|
|
revision: "master",
|
|
},
|
|
{
|
|
name: "should resolve tag without semantic versioning",
|
|
revision: "release-0.8",
|
|
expectedCommit: "ff87d8cb9e669d3738434733ecba3c6dd2c64d70",
|
|
},
|
|
{
|
|
name: "should resolve a pinned tag with semantic versioning",
|
|
revision: "v0.8.0",
|
|
expectedCommit: "d7c04ae24c16f8ec611b0331596fbc595537abe9",
|
|
},
|
|
{
|
|
name: "should resolve a range tag with semantic versioning",
|
|
revision: "v0.8.*", // it should resolve to v0.8.2
|
|
expectedCommit: "e5eefa2b943ae14a3e4491d4e35ef082e1c2a3f4",
|
|
},
|
|
{
|
|
name: "should resolve a range tag with semantic versioning without the 'v' prefix",
|
|
revision: "0.8.*", // it should resolve to v0.8.2
|
|
expectedCommit: "e5eefa2b943ae14a3e4491d4e35ef082e1c2a3f4",
|
|
},
|
|
{
|
|
name: "should resolve a conditional range tag with semantic versioning",
|
|
revision: ">=v2.9.0 <2.10.4", // it should resolve to v2.10.3
|
|
expectedCommit: "0fd6344537eb948cff602824a1d060421ceff40e",
|
|
},
|
|
{
|
|
name: "should resolve a star range tag with semantic versioning",
|
|
revision: "*",
|
|
},
|
|
{
|
|
name: "should resolve a star range suffixed tag with semantic versioning",
|
|
revision: "*-0",
|
|
},
|
|
{
|
|
name: "should resolve commit sha",
|
|
revision: "4e22a3cb21fa447ca362a05a505a69397c8a0d44",
|
|
expectedCommit: "4e22a3cb21fa447ca362a05a505a69397c8a0d44",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
commitSHA, err := clnt.LsRemote(tc.revision)
|
|
require.NoError(t, err)
|
|
assert.True(t, IsCommitSHA(commitSHA))
|
|
if tc.expectedCommit != "" {
|
|
assert.Equal(t, tc.expectedCommit, commitSHA)
|
|
}
|
|
})
|
|
}
|
|
|
|
// We do not resolve truncated git hashes and return the commit as-is if it appears to be a commit
|
|
t.Run("truncated commit", func(t *testing.T) {
|
|
commitSHA, err := clnt.LsRemote("4e22a3c")
|
|
require.NoError(t, err)
|
|
assert.False(t, IsCommitSHA(commitSHA))
|
|
assert.True(t, IsTruncatedCommitSHA(commitSHA))
|
|
})
|
|
|
|
t.Run("unresolvable revisions", func(t *testing.T) {
|
|
xfail := []string{
|
|
"unresolvable",
|
|
"4e22a3", // too short (6 characters)
|
|
}
|
|
|
|
for _, revision := range xfail {
|
|
_, err := clnt.LsRemote(revision)
|
|
assert.ErrorContains(t, err, "unable to resolve")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Running this test requires git-lfs to be installed on your machine.
|
|
func TestLFSClient(t *testing.T) {
|
|
setupGitEnv(t)
|
|
// temporary disable LFS test
|
|
// TODO(alexmt): dockerize tests in and enabled it
|
|
t.Skip()
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
client, err := NewClientExt("https://github.com/argoproj-labs/argocd-testrepo-lfs", tempDir, NopCreds{}, false, true, "", "")
|
|
require.NoError(t, err)
|
|
|
|
commitSHA, err := client.LsRemote("HEAD")
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, commitSHA)
|
|
|
|
err = client.Init()
|
|
require.NoError(t, err)
|
|
|
|
err = client.Fetch(t.Context(), "", 0)
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.Checkout(t.Context(), commitSHA, true, true)
|
|
require.NoError(t, err)
|
|
|
|
largeFiles, err := client.LsLargeFiles(t.Context())
|
|
require.NoError(t, err)
|
|
assert.Len(t, largeFiles, 3)
|
|
|
|
fileHandle, err := os.Open(tempDir + "/test3.yaml")
|
|
require.NoError(t, err)
|
|
if err == nil {
|
|
defer func() {
|
|
if err = fileHandle.Close(); err != nil {
|
|
require.NoError(t, err)
|
|
}
|
|
}()
|
|
text, err := io.ReadAll(fileHandle)
|
|
require.NoError(t, err)
|
|
if err == nil {
|
|
assert.Equal(t, "This is not a YAML, sorry.\n", string(text))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVerifyCommitSignature(t *testing.T) {
|
|
setupGitEnv(t)
|
|
p := t.TempDir()
|
|
|
|
client, err := NewClientExt("https://github.com/argoproj/argocd-example-apps.git", p, NopCreds{}, false, false, "", "")
|
|
require.NoError(t, err)
|
|
|
|
err = client.Init()
|
|
require.NoError(t, err)
|
|
|
|
// Use shallow fetch to avoid timeout fetching the entire repo
|
|
err = client.Fetch(t.Context(), "", 1)
|
|
require.NoError(t, err)
|
|
|
|
commitSHA, err := client.LsRemote("HEAD")
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.Checkout(t.Context(), commitSHA, true, true)
|
|
require.NoError(t, err)
|
|
|
|
// Fetch the specific commits needed for signature verification
|
|
signedCommit := "723b86e01bea11dcf72316cb172868fcbf05d69e"
|
|
unsignedCommit := "1ccdee0a611224ccc6b9ff7919fe7002f905436e"
|
|
err = client.Fetch(t.Context(), signedCommit, 1)
|
|
require.NoError(t, err)
|
|
err = client.Fetch(t.Context(), unsignedCommit, 1)
|
|
require.NoError(t, err)
|
|
|
|
// 28027897aad1262662096745f2ce2d4c74d02b7f is a commit that is signed in the repo
|
|
// It doesn't matter whether we know the key or not at this stage
|
|
{
|
|
out, err := client.VerifyCommitSignature(t.Context(), signedCommit)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, out)
|
|
assert.Contains(t, out, "gpg: Signature made")
|
|
}
|
|
|
|
// 85d660f0b967960becce3d49bd51c678ba2a5d24 is a commit that is not signed
|
|
{
|
|
out, err := client.VerifyCommitSignature(t.Context(), unsignedCommit)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, out)
|
|
}
|
|
}
|
|
|
|
func TestNewFactory(t *testing.T) {
|
|
setupGitEnv(t)
|
|
addBinDirToPath := path.NewBinDirToPath(t)
|
|
defer addBinDirToPath.Close()
|
|
closer := log.Debug()
|
|
defer closer()
|
|
type args struct {
|
|
url string
|
|
insecureIgnoreHostKey bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
}{
|
|
{"GitHub", args{url: "https://github.com/argoproj/argocd-example-apps"}},
|
|
}
|
|
for _, tt := range tests {
|
|
if tt.name == "PrivateSSHRepo" {
|
|
test.Flaky(t)
|
|
}
|
|
|
|
dirName := t.TempDir()
|
|
|
|
client, err := NewClientExt(tt.args.url, dirName, NopCreds{}, tt.args.insecureIgnoreHostKey, false, "", "")
|
|
require.NoError(t, err)
|
|
commitSHA, err := client.LsRemote("HEAD")
|
|
require.NoError(t, err)
|
|
|
|
err = client.Init()
|
|
require.NoError(t, err)
|
|
|
|
err = client.Fetch(t.Context(), "", 0)
|
|
require.NoError(t, err)
|
|
|
|
// Do a second fetch to make sure we can treat `already up-to-date` error as not an error
|
|
err = client.Fetch(t.Context(), "", 0)
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.Checkout(t.Context(), commitSHA, true, true)
|
|
require.NoError(t, err)
|
|
|
|
revisionMetadata, err := client.RevisionMetadata(t.Context(), commitSHA)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, revisionMetadata)
|
|
assert.Regexp(t, "^.*<.*>$", revisionMetadata.Author)
|
|
assert.Empty(t, revisionMetadata.Tags)
|
|
assert.NotEmpty(t, revisionMetadata.Date)
|
|
assert.NotEmpty(t, revisionMetadata.Message)
|
|
|
|
commitSHA2, err := client.CommitSHA(t.Context())
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, commitSHA, commitSHA2)
|
|
}
|
|
}
|
|
|
|
func TestListRevisions(t *testing.T) {
|
|
setupGitEnv(t)
|
|
dir := t.TempDir()
|
|
|
|
repoURL := "https://github.com/argoproj/argo-cd.git"
|
|
client, err := NewClientExt(repoURL, dir, NopCreds{}, false, false, "", "")
|
|
require.NoError(t, err)
|
|
|
|
lsResult, err := client.LsRefs()
|
|
require.NoError(t, err)
|
|
|
|
testBranch := "master"
|
|
testTag := "v1.0.0"
|
|
|
|
assert.Contains(t, lsResult.Branches, testBranch)
|
|
assert.Contains(t, lsResult.Tags, testTag)
|
|
assert.NotContains(t, lsResult.Branches, testTag)
|
|
assert.NotContains(t, lsResult.Tags, testBranch)
|
|
}
|
|
|
|
func TestLsFiles(t *testing.T) {
|
|
setupGitEnv(t)
|
|
tmpDir1 := t.TempDir()
|
|
tmpDir2 := t.TempDir()
|
|
ctx := t.Context()
|
|
|
|
client, err := NewClientExt("", tmpDir1, NopCreds{}, false, false, "", "")
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, runCmd(ctx, tmpDir1, "git", "init"))
|
|
|
|
// Setup files
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir1, "a.yaml"), []byte{}, 0o644))
|
|
require.NoError(t, os.MkdirAll(filepath.Join(tmpDir1, "subdir"), 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir1, "subdir", "b.yaml"), []byte{}, 0o644))
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(tmpDir2, "subdir"), 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(tmpDir2, "c.yaml"), []byte{}, 0o644))
|
|
|
|
require.NoError(t, os.Symlink(filepath.Join(tmpDir2, "c.yaml"), filepath.Join(tmpDir1, "link.yaml")))
|
|
|
|
require.NoError(t, runCmd(ctx, tmpDir1, "git", "add", "."))
|
|
require.NoError(t, runCmd(ctx, tmpDir1, "git", "commit", "-m", "Initial commit"))
|
|
|
|
tests := []struct {
|
|
name string
|
|
pattern string
|
|
safeGlobbing bool
|
|
expectedResult []string
|
|
}{
|
|
{
|
|
name: "Old globbing with symlinks and subdir",
|
|
pattern: "*.yaml",
|
|
safeGlobbing: false,
|
|
expectedResult: []string{"a.yaml", "link.yaml", "subdir/b.yaml"},
|
|
},
|
|
{
|
|
name: "Safe globbing excludes symlinks",
|
|
pattern: "*.yaml",
|
|
safeGlobbing: true,
|
|
expectedResult: []string{"a.yaml"},
|
|
},
|
|
{
|
|
name: "Safe globbing excludes external paths",
|
|
pattern: filepath.Join(tmpDir2, "*.yaml"),
|
|
safeGlobbing: true,
|
|
expectedResult: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
lsResult, err := client.LsFiles(t.Context(), tt.pattern, tt.safeGlobbing)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expectedResult, lsResult)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLsFilesForGitFileGeneratorGlobbingPatterns(t *testing.T) {
|
|
setupGitEnv(t)
|
|
tmpDir := t.TempDir()
|
|
ctx := t.Context()
|
|
|
|
client, err := NewClientExt("", tmpDir, NopCreds{}, false, false, "", "")
|
|
require.NoError(t, err)
|
|
|
|
err = runCmd(ctx, tmpDir, "git", "init")
|
|
require.NoError(t, err)
|
|
|
|
// Setup directory structure and files
|
|
files := []string{
|
|
"cluster-charts/cluster1/mychart/charts/mysubchart/values.yaml",
|
|
"cluster-charts/cluster1/mychart/values.yaml",
|
|
"cluster-charts/cluster1/myotherchart/values.yaml",
|
|
"cluster-charts/cluster2/values.yaml",
|
|
"some-path/values.yaml",
|
|
"some-path/staging/values.yaml",
|
|
"cluster-config/engineering/production/config.json",
|
|
"cluster-config/engineering/dev/config.json",
|
|
"p1/p2/config.json",
|
|
"p1/app2/config.json",
|
|
"p1/app3/config.json",
|
|
"p1/config.json",
|
|
}
|
|
for _, file := range files {
|
|
dir := filepath.Dir(file)
|
|
require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, dir), 0o755))
|
|
_, err := os.Create(filepath.Join(tmpDir, file))
|
|
require.NoError(t, err)
|
|
}
|
|
require.NoError(t, runCmd(ctx, tmpDir, "git", "add", "."))
|
|
require.NoError(t, runCmd(ctx, tmpDir, "git", "commit", "-m", "Initial commit"))
|
|
|
|
tests := []struct {
|
|
name string
|
|
pattern string
|
|
isNewGlobbingEnabled bool
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "**/config.json (isNewGlobbingEnabled)",
|
|
pattern: "**/config.json",
|
|
isNewGlobbingEnabled: true,
|
|
expected: []string{
|
|
"cluster-config/engineering/production/config.json",
|
|
"cluster-config/engineering/dev/config.json",
|
|
"p1/config.json",
|
|
"p1/p2/config.json",
|
|
"p1/app2/config.json",
|
|
"p1/app3/config.json",
|
|
},
|
|
},
|
|
{
|
|
name: "**/config.json (non-isNewGlobbingEnabled)",
|
|
pattern: "**/config.json",
|
|
isNewGlobbingEnabled: false,
|
|
expected: []string{
|
|
"cluster-config/engineering/production/config.json",
|
|
"cluster-config/engineering/dev/config.json",
|
|
"p1/config.json",
|
|
"p1/p2/config.json",
|
|
"p1/app2/config.json",
|
|
"p1/app3/config.json",
|
|
},
|
|
},
|
|
{
|
|
name: "some-path/*.yaml (isNewGlobbingEnabled)",
|
|
pattern: "some-path/*.yaml",
|
|
isNewGlobbingEnabled: true,
|
|
expected: []string{"some-path/values.yaml"},
|
|
},
|
|
{
|
|
name: "some-path/*.yaml (non-isNewGlobbingEnabled)",
|
|
pattern: "some-path/*.yaml",
|
|
isNewGlobbingEnabled: false,
|
|
expected: []string{
|
|
"some-path/values.yaml",
|
|
"some-path/staging/values.yaml",
|
|
},
|
|
},
|
|
{
|
|
name: "p1/**/config.json (isNewGlobbingEnabled)",
|
|
pattern: "p1/**/config.json",
|
|
isNewGlobbingEnabled: true,
|
|
expected: []string{
|
|
"p1/config.json",
|
|
"p1/p2/config.json",
|
|
"p1/app2/config.json",
|
|
"p1/app3/config.json",
|
|
},
|
|
},
|
|
{
|
|
name: "p1/**/config.json (non-isNewGlobbingEnabled)",
|
|
pattern: "p1/**/config.json",
|
|
isNewGlobbingEnabled: false,
|
|
expected: []string{
|
|
"p1/p2/config.json",
|
|
"p1/app2/config.json",
|
|
"p1/app3/config.json",
|
|
},
|
|
},
|
|
{
|
|
name: "cluster-config/**/config.json (isNewGlobbingEnabled)",
|
|
pattern: "cluster-config/**/config.json",
|
|
isNewGlobbingEnabled: true,
|
|
expected: []string{
|
|
"cluster-config/engineering/production/config.json",
|
|
"cluster-config/engineering/dev/config.json",
|
|
},
|
|
},
|
|
{
|
|
name: "cluster-config/**/config.json (isNewGlobbingEnabled=false)",
|
|
pattern: "cluster-config/**/config.json",
|
|
isNewGlobbingEnabled: false,
|
|
expected: []string{
|
|
"cluster-config/engineering/dev/config.json",
|
|
"cluster-config/engineering/production/config.json",
|
|
},
|
|
},
|
|
{
|
|
name: "cluster-config/*/dev/config.json (isNewGlobbingEnabled)",
|
|
pattern: "cluster-config/*/dev/config.json",
|
|
isNewGlobbingEnabled: true,
|
|
expected: []string{"cluster-config/engineering/dev/config.json"},
|
|
},
|
|
{
|
|
name: "cluster-config/*/dev/config.json (isNewGlobbingEnabled=false)",
|
|
pattern: "cluster-config/*/dev/config.json",
|
|
isNewGlobbingEnabled: false,
|
|
expected: []string{"cluster-config/engineering/dev/config.json"},
|
|
},
|
|
{
|
|
name: "cluster-charts/*/*/values.yaml (isNewGlobbingEnabled)",
|
|
pattern: "cluster-charts/*/*/values.yaml",
|
|
isNewGlobbingEnabled: true,
|
|
expected: []string{
|
|
"cluster-charts/cluster1/mychart/values.yaml",
|
|
"cluster-charts/cluster1/myotherchart/values.yaml",
|
|
},
|
|
},
|
|
{
|
|
name: "cluster-charts/*/*/values.yaml (isNewGlobbingEnabled=false)",
|
|
pattern: "cluster-charts/*/*/values.yaml",
|
|
isNewGlobbingEnabled: false,
|
|
expected: []string{
|
|
"cluster-charts/cluster1/mychart/values.yaml",
|
|
"cluster-charts/cluster1/myotherchart/values.yaml",
|
|
"cluster-charts/cluster1/mychart/charts/mysubchart/values.yaml",
|
|
},
|
|
},
|
|
{
|
|
name: "cluster-charts/*/values.yaml (isNewGlobbingEnabled)",
|
|
pattern: "cluster-charts/*/values.yaml",
|
|
isNewGlobbingEnabled: true,
|
|
expected: []string{
|
|
"cluster-charts/cluster2/values.yaml",
|
|
},
|
|
},
|
|
{
|
|
name: "cluster-charts/*/values.yaml (non-isNewGlobbingEnabled)",
|
|
pattern: "cluster-charts/*/values.yaml",
|
|
isNewGlobbingEnabled: false,
|
|
expected: []string{
|
|
"cluster-charts/cluster2/values.yaml",
|
|
"cluster-charts/cluster1/mychart/values.yaml",
|
|
"cluster-charts/cluster1/myotherchart/values.yaml",
|
|
"cluster-charts/cluster1/mychart/charts/mysubchart/values.yaml",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
lsResult, err := client.LsFiles(t.Context(), tt.pattern, tt.isNewGlobbingEnabled)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, tt.expected, lsResult)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAnnotatedTagHandling(t *testing.T) {
|
|
setupGitEnv(t)
|
|
dir := t.TempDir()
|
|
|
|
client, err := NewClientExt("https://github.com/argoproj/argo-cd.git", dir, NopCreds{}, false, false, "", "")
|
|
require.NoError(t, err)
|
|
|
|
err = client.Init()
|
|
require.NoError(t, err)
|
|
|
|
// Test annotated tag resolution
|
|
commitSHA, err := client.LsRemote("v1.0.0") // Known annotated tag
|
|
require.NoError(t, err)
|
|
|
|
// Verify we get commit SHA, not tag SHA
|
|
assert.True(t, IsCommitSHA(commitSHA))
|
|
|
|
// Test tag reference handling
|
|
refs, err := client.LsRefs()
|
|
require.NoError(t, err)
|
|
|
|
// Verify tag exists in the list and points to a valid commit SHA
|
|
assert.Contains(t, refs.Tags, "v1.0.0", "Tag v1.0.0 should exist in refs")
|
|
}
|
|
|
|
func TestIsShortRef(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
revision string
|
|
expected bool
|
|
}{
|
|
// Short refs (should return true)
|
|
{
|
|
name: "short branch name",
|
|
revision: "master",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "short branch name with slashes",
|
|
revision: "feature/my-feature",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "short branch name with multiple slashes",
|
|
revision: "release/v1.0/bugfix",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "short tag name",
|
|
revision: "v1.0.0",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "short tag name without version prefix",
|
|
revision: "release-1.0",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "simple branch name",
|
|
revision: "main",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "branch with hyphens",
|
|
revision: "my-feature-branch",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "branch with underscores",
|
|
revision: "my_feature_branch",
|
|
expected: true,
|
|
},
|
|
// Full refs (should return false)
|
|
{
|
|
name: "full branch ref",
|
|
revision: "refs/heads/master",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "full tag ref",
|
|
revision: "refs/tags/v1.0.0",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "full remote branch ref",
|
|
revision: "refs/remotes/origin/master",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "full branch ref with slashes in name",
|
|
revision: "refs/heads/feature/my-feature",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "full tag ref with slashes in name",
|
|
revision: "refs/tags/release/v1.0.0",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "full remote branch ref with slashes",
|
|
revision: "refs/remotes/origin/feature/branch",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "full pull request ref",
|
|
revision: "refs/pull/123/head",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "full notes ref",
|
|
revision: "refs/notes/commits",
|
|
expected: false,
|
|
},
|
|
// Special cases
|
|
{
|
|
name: "HEAD",
|
|
revision: "HEAD",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "commit SHA",
|
|
revision: "9d921f65f3c5373b682e2eb4b37afba6592e8f8b",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "truncated commit SHA",
|
|
revision: "9d921f6",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "empty string",
|
|
revision: "",
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
result := IsShortRef(tt.revision)
|
|
assert.Equal(t, tt.expected, result, "IsShortRef(%q) = %v, expected %v", tt.revision, result, tt.expected)
|
|
})
|
|
}
|
|
}
|