mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
go.mod still declared `module github.com/hanzoai/deploy` after the repo was renamed. A Go module path is not cosmetic — it is the resolvable location. So `go get github.com/hanzoai/cd` could not work, and anything importing this module had to spell a name that no longer exists at that address. The build log of v3.6.0 is where it surfaced: the compiler was still emitting github.com/hanzoai/deploy/cmd/cd/commands. 3541 import paths across 690 .go, 19 .proto, 4 .yaml, 3 .sh, 2 .mod, Procfile and Makefile. Plus the 62 ghcr.io/hanzoai/deploy image references, moved to ghcr.io/hanzoai/cd to match hanzo.yml's build target — a distinct string from the module path, replaced separately so neither could clobber the other. go mod tidy clean, CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./... clean. Co-authored-by: Hanzo Dev <[email protected]>
394 lines
11 KiB
Go
394 lines
11 KiB
Go
package cd
|
|
|
|
import (
|
|
"bytes"
|
|
"sync"
|
|
"testing"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
|
|
argoappv1 "github.com/hanzoai/cd/pkg/apis/application/v1alpha1"
|
|
)
|
|
|
|
const (
|
|
_somecomponent = "somecomponent"
|
|
_test = "test"
|
|
_cdNs = "cd"
|
|
_targetNs = "target-namespace"
|
|
)
|
|
|
|
var testEnableEventLog = []string{_somecomponent, _test}
|
|
|
|
// logCaptureMutex ensures that only one captureLogEntries call can run at a time
|
|
// to prevent log entries from parallel test runs from ending up in wrong buffers
|
|
var logCaptureMutex sync.Mutex
|
|
|
|
// ThreadSafeBuffer wraps bytes.Buffer with a mutex to make it thread-safe
|
|
type ThreadSafeBuffer struct {
|
|
buf bytes.Buffer
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func (tsb *ThreadSafeBuffer) Write(p []byte) (n int, err error) {
|
|
tsb.mu.Lock()
|
|
defer tsb.mu.Unlock()
|
|
return tsb.buf.Write(p)
|
|
}
|
|
|
|
func (tsb *ThreadSafeBuffer) String() string {
|
|
tsb.mu.RLock()
|
|
defer tsb.mu.RUnlock()
|
|
return tsb.buf.String()
|
|
}
|
|
|
|
// Helper to capture log entries generated by the logger and return it as string
|
|
func captureLogEntries(run func()) string {
|
|
logCaptureMutex.Lock()
|
|
defer logCaptureMutex.Unlock()
|
|
|
|
f := log.StandardLogger().Formatter
|
|
log.SetFormatter(&log.TextFormatter{DisableColors: true})
|
|
defer log.SetFormatter(f)
|
|
|
|
output := &ThreadSafeBuffer{}
|
|
original := log.StandardLogger().Out
|
|
log.SetOutput(output)
|
|
defer log.SetOutput(original)
|
|
|
|
run()
|
|
|
|
return output.String()
|
|
}
|
|
|
|
func TestNewAuditLogger(t *testing.T) {
|
|
t.Parallel()
|
|
logger := NewAuditLogger(fake.NewClientset(), _cdNs, _somecomponent, testEnableEventLog)
|
|
assert.NotNil(t, logger)
|
|
assert.Equal(t, _cdNs, logger.namespace)
|
|
assert.Equal(t, _somecomponent, logger.component)
|
|
}
|
|
|
|
func TestLogAppProjEvent(t *testing.T) {
|
|
t.Parallel()
|
|
fakeClient := fake.NewClientset()
|
|
logger := NewAuditLogger(fakeClient, _cdNs, _somecomponent, testEnableEventLog)
|
|
assert.NotNil(t, logger)
|
|
|
|
proj := argoappv1.AppProject{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "default",
|
|
Namespace: _cdNs,
|
|
ResourceVersion: "1",
|
|
UID: "a-b-c-d-e",
|
|
},
|
|
Spec: argoappv1.AppProjectSpec{
|
|
Description: "Test project",
|
|
},
|
|
}
|
|
|
|
ei := EventInfo{
|
|
Reason: _test,
|
|
Type: "info",
|
|
}
|
|
|
|
captureLogEntries(func() {
|
|
logger.LogAppProjEvent(&proj, ei, "This is a test message", "admin")
|
|
})
|
|
|
|
// Verify event was created in the AppProject's namespace (cd)
|
|
events, err := fakeClient.CoreV1().Events(_cdNs).List(t.Context(), metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, events.Items, 1)
|
|
|
|
event := events.Items[0]
|
|
assert.Equal(t, "default", event.InvolvedObject.Name)
|
|
assert.Equal(t, _cdNs, event.InvolvedObject.Namespace)
|
|
assert.Equal(t, _cdNs, event.Namespace) // Event created in cd namespace
|
|
assert.Equal(t, "This is a test message", event.Message)
|
|
assert.Equal(t, "test", event.Reason)
|
|
assert.Equal(t, "info", event.Type)
|
|
}
|
|
|
|
func TestLogAppEvent(t *testing.T) {
|
|
t.Parallel()
|
|
fakeClient := fake.NewClientset()
|
|
logger := NewAuditLogger(fakeClient, _cdNs, _somecomponent, testEnableEventLog)
|
|
assert.NotNil(t, logger)
|
|
|
|
app := argoappv1.Application{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "testapp",
|
|
Namespace: _cdNs,
|
|
ResourceVersion: "1",
|
|
UID: "a-b-c-d-e",
|
|
},
|
|
Spec: argoappv1.ApplicationSpec{
|
|
Destination: argoappv1.ApplicationDestination{
|
|
Server: "https://127.0.0.1:6443",
|
|
Namespace: "testns",
|
|
},
|
|
},
|
|
}
|
|
|
|
ei := EventInfo{
|
|
Reason: _test,
|
|
Type: "info",
|
|
}
|
|
captureLogEntries(func() {
|
|
logger.LogAppEvent(&app, ei, "This is a test message", "admin", nil)
|
|
})
|
|
|
|
// Verify event was created in the Application's namespace (cd)
|
|
events, err := fakeClient.CoreV1().Events(_cdNs).List(t.Context(), metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, events.Items, 1)
|
|
|
|
event := events.Items[0]
|
|
assert.Equal(t, "testapp", event.InvolvedObject.Name)
|
|
assert.Equal(t, _cdNs, event.InvolvedObject.Namespace)
|
|
assert.Equal(t, _cdNs, event.Namespace) // Event created in cd namespace
|
|
assert.Equal(t, "This is a test message", event.Message)
|
|
assert.Equal(t, "test", event.Reason)
|
|
assert.Equal(t, "info", event.Type)
|
|
assert.Equal(t, "admin", event.Annotations["user"])
|
|
assert.Equal(t, "https://127.0.0.1:6443", event.Annotations["dest-server"])
|
|
assert.Equal(t, "testns", event.Annotations["dest-namespace"])
|
|
|
|
// If K8s Event Disable Log
|
|
ei.Reason = "Unknown"
|
|
output := captureLogEntries(func() {
|
|
logger.LogAppEvent(&app, ei, "This is a test message", "", nil)
|
|
})
|
|
|
|
assert.Empty(t, output)
|
|
}
|
|
|
|
func TestLogResourceEvent(t *testing.T) {
|
|
t.Parallel()
|
|
logger := NewAuditLogger(fake.NewClientset(), _cdNs, _somecomponent, testEnableEventLog)
|
|
assert.NotNil(t, logger)
|
|
|
|
res := argoappv1.ResourceNode{
|
|
ResourceRef: argoappv1.ResourceRef{
|
|
Group: "cd.hanzo.ai",
|
|
Version: "v1alpha1",
|
|
Kind: "SignatureKey",
|
|
Name: "testapp",
|
|
Namespace: _cdNs,
|
|
UID: "a-b-c-d-e",
|
|
},
|
|
}
|
|
|
|
ei := EventInfo{
|
|
Reason: _test,
|
|
Type: "info",
|
|
}
|
|
|
|
output := captureLogEntries(func() {
|
|
logger.LogResourceEvent(&res, ei, "This is a test message", "")
|
|
})
|
|
|
|
assert.Contains(t, output, "level=info")
|
|
assert.Contains(t, output, "name=testapp")
|
|
assert.Contains(t, output, "reason=test")
|
|
assert.Contains(t, output, "type=info")
|
|
assert.Contains(t, output, "msg=\"This is a test message\"")
|
|
|
|
ei.Reason = "Unknown"
|
|
|
|
// If K8s Event Disable Log
|
|
output = captureLogEntries(func() {
|
|
logger.LogResourceEvent(&res, ei, "This is a test message", "")
|
|
})
|
|
|
|
assert.Empty(t, output)
|
|
}
|
|
|
|
func TestLogResourceEvent_MultiCluster_CreatesEventInArgocdNamespace(t *testing.T) {
|
|
t.Parallel()
|
|
fakeClient := fake.NewClientset()
|
|
logger := NewAuditLogger(fakeClient, _cdNs, _somecomponent, []string{EventReasonResourceActionRan})
|
|
|
|
res := argoappv1.ResourceNode{
|
|
ResourceRef: argoappv1.ResourceRef{
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Deployment",
|
|
Name: "my-deployment",
|
|
Namespace: _targetNs, // Resource is in a different namespace/cluster
|
|
UID: "deploy-uid-123",
|
|
},
|
|
}
|
|
|
|
ei := EventInfo{
|
|
Reason: EventReasonResourceActionRan,
|
|
Type: corev1.EventTypeNormal,
|
|
}
|
|
captureLogEntries(func() {
|
|
logger.LogResourceEvent(&res, ei, "Resource action executed", "admin")
|
|
})
|
|
|
|
events, err := fakeClient.CoreV1().Events(_targetNs).List(t.Context(), metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, events.Items, "No event should be created in target namespace")
|
|
|
|
events, err = fakeClient.CoreV1().Events(_cdNs).List(t.Context(), metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, events.Items, 1, "Event should be created in ArgoCD namespace")
|
|
|
|
event := events.Items[0]
|
|
assert.Equal(t, "my-deployment", event.InvolvedObject.Name)
|
|
assert.Equal(t, _targetNs, event.InvolvedObject.Namespace, "InvolvedObject should preserve original namespace")
|
|
assert.Equal(t, _cdNs, event.Namespace, "Event itself should be in ArgoCD namespace")
|
|
assert.Equal(t, "Resource action executed", event.Message)
|
|
|
|
assert.Equal(t, _targetNs, event.Annotations["resource-namespace"])
|
|
}
|
|
|
|
func TestLogAppSetEvent_CreatesEventInAppSetNamespace(t *testing.T) {
|
|
t.Parallel()
|
|
fakeClient := fake.NewClientset()
|
|
logger := NewAuditLogger(fakeClient, _cdNs, _somecomponent, testEnableEventLog)
|
|
|
|
appset := argoappv1.ApplicationSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "my-appset",
|
|
Namespace: _cdNs,
|
|
ResourceVersion: "1",
|
|
UID: "appset-uid-123",
|
|
},
|
|
}
|
|
|
|
ei := EventInfo{
|
|
Reason: _test,
|
|
Type: corev1.EventTypeNormal,
|
|
}
|
|
captureLogEntries(func() {
|
|
logger.LogAppSetEvent(&appset, ei, "ApplicationSet event test", "admin")
|
|
})
|
|
|
|
// Verify event was created in the ApplicationSet's namespace (cd)
|
|
events, err := fakeClient.CoreV1().Events(_cdNs).List(t.Context(), metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, events.Items, 1)
|
|
|
|
event := events.Items[0]
|
|
assert.Equal(t, "my-appset", event.InvolvedObject.Name)
|
|
assert.Equal(t, _cdNs, event.InvolvedObject.Namespace)
|
|
assert.Equal(t, _cdNs, event.Namespace) // Event created in cd namespace
|
|
assert.Equal(t, "ApplicationSet event test", event.Message)
|
|
}
|
|
|
|
func TestLogResourceEvent_DifferentKinds_AllInArgocdNamespace(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
resourceKind string
|
|
resourceGroup string
|
|
resourceNs string
|
|
}{
|
|
{
|
|
name: "Pod in remote namespace",
|
|
resourceKind: "Pod",
|
|
resourceGroup: "",
|
|
resourceNs: "production",
|
|
},
|
|
{
|
|
name: "Service in remote namespace",
|
|
resourceKind: "Service",
|
|
resourceGroup: "",
|
|
resourceNs: "staging",
|
|
},
|
|
{
|
|
name: "Deployment in remote namespace",
|
|
resourceKind: "Deployment",
|
|
resourceGroup: "apps",
|
|
resourceNs: "default",
|
|
},
|
|
{
|
|
name: "ConfigMap in remote namespace",
|
|
resourceKind: "ConfigMap",
|
|
resourceGroup: "",
|
|
resourceNs: "kube-system",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
fakeClient := fake.NewClientset()
|
|
logger := NewAuditLogger(fakeClient, _cdNs, _somecomponent, []string{EventReasonResourceActionRan})
|
|
|
|
res := argoappv1.ResourceNode{
|
|
ResourceRef: argoappv1.ResourceRef{
|
|
Group: tc.resourceGroup,
|
|
Version: "v1",
|
|
Kind: tc.resourceKind,
|
|
Name: "test-resource",
|
|
Namespace: tc.resourceNs,
|
|
UID: "test-uid",
|
|
},
|
|
}
|
|
|
|
ei := EventInfo{
|
|
Reason: EventReasonResourceActionRan,
|
|
Type: corev1.EventTypeNormal,
|
|
}
|
|
captureLogEntries(func() {
|
|
logger.LogResourceEvent(&res, ei, "Action ran", "admin")
|
|
})
|
|
|
|
events, err := fakeClient.CoreV1().Events(_cdNs).List(t.Context(), metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, events.Items, 1)
|
|
|
|
event := events.Items[0]
|
|
assert.Equal(t, _cdNs, event.Namespace, "Event should be in ArgoCD namespace for %s", tc.resourceKind)
|
|
assert.Equal(t, tc.resourceNs, event.InvolvedObject.Namespace, "InvolvedObject should preserve original namespace")
|
|
assert.Equal(t, tc.resourceNs, event.Annotations["resource-namespace"])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogResourceEvent_EmptyNamespace(t *testing.T) {
|
|
t.Parallel()
|
|
fakeClient := fake.NewClientset()
|
|
logger := NewAuditLogger(fakeClient, _cdNs, _somecomponent, []string{EventReasonResourceActionRan})
|
|
|
|
// Cluster-scoped resource (no namespace)
|
|
res := argoappv1.ResourceNode{
|
|
ResourceRef: argoappv1.ResourceRef{
|
|
Group: "rbac.authorization.k8s.io",
|
|
Version: "v1",
|
|
Kind: "ClusterRole",
|
|
Name: "cluster-admin",
|
|
Namespace: "", // Cluster-scoped resource
|
|
UID: "clusterrole-uid",
|
|
},
|
|
}
|
|
|
|
ei := EventInfo{
|
|
Reason: EventReasonResourceActionRan,
|
|
Type: corev1.EventTypeNormal,
|
|
}
|
|
captureLogEntries(func() {
|
|
logger.LogResourceEvent(&res, ei, "Cluster role action", "admin")
|
|
})
|
|
|
|
// Event should be created in ArgoCD namespace (not empty namespace)
|
|
events, err := fakeClient.CoreV1().Events(_cdNs).List(t.Context(), metav1.ListOptions{})
|
|
require.NoError(t, err)
|
|
require.Len(t, events.Items, 1)
|
|
|
|
event := events.Items[0]
|
|
assert.Equal(t, _cdNs, event.Namespace)
|
|
assert.Empty(t, event.InvolvedObject.Namespace) // ClusterRole has no namespace
|
|
assert.Empty(t, event.Annotations["resource-namespace"])
|
|
}
|