Files
zeekayandHanzo Dev 1ab6e799c1 cd: the module is github.com/hanzoai/cd, where it is actually hosted
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]>
2026-07-28 21:16:24 -07:00

550 lines
18 KiB
Go

package kube
import (
"encoding/json"
"testing"
"github.com/hanzoai/cd/gitops-engine/pkg/utils/kube/mocks"
testingutils "github.com/hanzoai/cd/gitops-engine/pkg/utils/testing"
"github.com/hanzoai/cd/gitops-engine/pkg/utils/tracing"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/client-go/kubernetes"
kubefake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/rest"
"k8s.io/kubectl/pkg/cmd/apply"
"k8s.io/kubectl/pkg/cmd/create"
"k8s.io/kubectl/pkg/cmd/replace"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
func newTestKubectlResourceOperations(t *testing.T) (*kubectlResourceOperations, *mocks.KubectlOptionsRunner) {
t.Helper()
cmdMocks := mocks.NewKubectlOptionsRunner(t)
k := &kubectlResourceOperations{
config: &rest.Config{},
log: logr.Discard(),
tracer: &tracing.NopTracer{},
fact: cmdutil.NewFactory(cmdutil.NewMatchVersionFlags(genericclioptions.NewConfigFlags(true))),
optionsRunner: cmdMocks,
getClientFunc: func() (kubernetes.Interface, error) {
return kubefake.NewSimpleClientset(), nil
},
outputMode: outputModeLog,
}
return k, cmdMocks
}
func TestAuthReconcileWithMissingNamespace(t *testing.T) {
t.Parallel()
namespace := "test-ns"
t.Run("Namespaced resources", func(t *testing.T) {
t.Parallel()
k, _ := newTestKubectlResourceOperations(t)
role := testingutils.NewRole()
role.SetNamespace(namespace)
_, err := k.rbacReconcile(t.Context(), role, cmdutil.DryRunNone)
require.Error(t, err)
assert.Contains(t, err.Error(), `namespaces "test-ns" not found`)
roleBinding := testingutils.NewRoleBinding()
roleBinding.SetNamespace(namespace)
_, err = k.rbacReconcile(t.Context(), roleBinding, cmdutil.DryRunNone)
require.Error(t, err)
assert.Contains(t, err.Error(), `namespaces "test-ns" not found`)
})
t.Run("Cluster-scoped resources", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("AuthReconcile", mock.Anything).Return(nil).Twice()
clusterRole := testingutils.NewClusterRole()
clusterRole.SetNamespace(namespace)
_, err := k.rbacReconcile(t.Context(), clusterRole, cmdutil.DryRunNone)
require.NoError(t, err)
clusterRoleBinding := testingutils.NewClusterRoleBinding()
clusterRoleBinding.SetNamespace(namespace)
_, err = k.rbacReconcile(t.Context(), clusterRoleBinding, cmdutil.DryRunNone)
require.NoError(t, err)
})
}
func TestAuthReconcileUsage(t *testing.T) {
t.Parallel()
// This test verifies that the rbacReconcile logic is correctly applied based on the operation type
// and server-side apply setting.
role := testingutils.NewRole()
t.Run("CreateResource should not call auth reconcile", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(nil)
_, err := k.CreateResource(t.Context(), role, cmdutil.DryRunNone, false)
require.NoError(t, err)
cmdMocks.AssertNotCalled(t, "AuthReconcile")
})
t.Run("ReplaceResource should not call auth reconcile", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("Replace", mock.Anything, mock.Anything).Return(nil)
_, err := k.ReplaceResource(t.Context(), role, cmdutil.DryRunNone, false)
require.NoError(t, err)
cmdMocks.AssertNotCalled(t, "AuthReconcile")
})
t.Run("ApplyResource should not call auth reconcile on server-side apply", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("Apply", mock.Anything).Return(nil)
ssa := true
_, err := k.ApplyResource(t.Context(), role, cmdutil.DryRunNone, false, false, ssa, "")
require.NoError(t, err)
cmdMocks.AssertNotCalled(t, "AuthReconcile")
})
t.Run("ApplyResource should call auth reconcile on client-side apply", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("Apply", mock.Anything).Return(nil)
cmdMocks.On("AuthReconcile", mock.Anything).Return(nil)
ssa := false
_, err := k.ApplyResource(t.Context(), role, cmdutil.DryRunNone, false, false, ssa, "")
require.NoError(t, err)
})
}
func TestOutputModeLog(t *testing.T) {
t.Parallel()
// Test normal flow operations with outputModeLog
t.Run("CreateResource with outputModeLog", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(nil)
obj := testingutils.NewPod()
_, err := k.CreateResource(t.Context(), obj, cmdutil.DryRunNone, false)
require.NoError(t, err)
})
t.Run("ReplaceResource with outputModeLog", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("Replace", mock.Anything, mock.Anything).Return(nil)
obj := testingutils.NewPod()
_, err := k.ReplaceResource(t.Context(), obj, cmdutil.DryRunNone, false)
require.NoError(t, err)
})
t.Run("ApplyResource with outputModeLog and client-side apply", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("Apply", mock.Anything).Return(nil)
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunNone, false, false, false, "test-manager")
require.NoError(t, err)
})
t.Run("ApplyResource with outputModeLog and server-side apply", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
cmdMocks.On("Apply", mock.Anything).Return(nil)
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunNone, false, false, true, "test-manager")
require.NoError(t, err)
})
}
func TestOutputModeJSON(t *testing.T) {
t.Parallel()
// Test JSON output mode operations
t.Run("CreateResource with outputModeJSON should fail", func(t *testing.T) {
t.Parallel()
k, _ := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
obj := testingutils.NewPod()
_, err := k.CreateResource(t.Context(), obj, cmdutil.DryRunServer, false)
require.Error(t, err)
assert.Contains(t, err.Error(), "CreateResource is not supported with JSON output mode")
})
t.Run("ReplaceResource with outputModeJSON should fail", func(t *testing.T) {
t.Parallel()
k, _ := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
obj := testingutils.NewPod()
_, err := k.ReplaceResource(t.Context(), obj, cmdutil.DryRunServer, false)
require.Error(t, err)
assert.Contains(t, err.Error(), "ReplaceResource is not supported with JSON output mode")
})
t.Run("ApplyResource with outputModeJSON without Dry run", func(t *testing.T) {
t.Parallel()
k, _ := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunNone, false, false, true, "test-manager")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid dry run strategy used with JSON output")
})
t.Run("ApplyResource with outputModeJSON requires DryRunServer", func(t *testing.T) {
t.Parallel()
k, _ := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunClient, false, false, true, "test-manager")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid dry run strategy used with JSON output")
})
t.Run("ApplyResource with outputModeJSON requires server-side apply", func(t *testing.T) {
t.Parallel()
k, _ := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunServer, false, false, false, "test-manager")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid Apply strategy used with JSON output")
})
t.Run("ApplyResource with outputModeJSON return object", func(t *testing.T) {
t.Parallel()
obj := testingutils.NewPod()
jsonObj, err := json.Marshal(obj)
require.NoError(t, err)
k, cmdMocks := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
cmdMocks.On("Apply", mock.Anything).Run(func(args mock.Arguments) {
applyOpts := args[0].(*apply.ApplyOptions)
_, err := applyOpts.Out.Write(jsonObj)
require.NoError(t, err)
}).Return(nil)
result, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunServer, false, false, true, "test-manager")
require.NoError(t, err)
assert.JSONEq(t, string(jsonObj), result)
})
t.Run("ApplyResource with outputModeJSON with object and stderr returns object", func(t *testing.T) {
t.Parallel()
obj := testingutils.NewPod()
jsonObj, err := json.Marshal(obj)
require.NoError(t, err)
k, cmdMocks := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
cmdMocks.On("Apply", mock.Anything).Run(func(args mock.Arguments) {
applyOpts := args[0].(*apply.ApplyOptions)
_, err := applyOpts.Out.Write(jsonObj)
require.NoError(t, err)
// add an stderr message that should not be returned in the result
_, err = applyOpts.ErrOut.Write([]byte("error message"))
require.NoError(t, err)
}).Return(nil)
result, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunServer, false, false, true, "test-manager")
require.NoError(t, err)
assert.JSONEq(t, string(jsonObj), result)
})
t.Run("ApplyResource with outputModeJSON without object with a stderr returns error", func(t *testing.T) {
t.Parallel()
obj := testingutils.NewPod()
k, cmdMocks := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
cmdMocks.On("Apply", mock.Anything).Run(func(args mock.Arguments) {
applyOpts := args[0].(*apply.ApplyOptions)
_, err := applyOpts.ErrOut.Write([]byte("error message"))
require.NoError(t, err)
}).Return(nil)
_, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunServer, false, false, true, "test-manager")
require.Error(t, err)
assert.Contains(t, err.Error(), "error message")
})
}
func TestApplyOptionsConfiguration(t *testing.T) {
t.Parallel()
// Test that newApplyOptions correctly configures all ApplyOptions fields
t.Run("general options are correctly set", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
strategy cmdutil.DryRunStrategy
}{
{"DryRunNone", cmdutil.DryRunNone},
{"DryRunClient", cmdutil.DryRunClient},
{"DryRunServer", cmdutil.DryRunServer},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
var capturedOpts *apply.ApplyOptions
cmdMocks.On("Apply", mock.Anything).Run(func(args mock.Arguments) {
capturedOpts = args[0].(*apply.ApplyOptions)
}).Return(nil)
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, tc.strategy, false, false, false, "test-manager")
require.NoError(t, err)
assert.Equal(t, tc.strategy, capturedOpts.DryRunStrategy)
assert.Equal(t, "test-manager", capturedOpts.FieldManager)
assert.True(t, capturedOpts.Overwrite)
if tc.strategy == cmdutil.DryRunClient {
// workaround for https://github.com/kubernetes/kubernetes/issues/139538
// in kubectl v1.36
assert.False(t, capturedOpts.OpenAPIPatch)
} else {
assert.True(t, capturedOpts.OpenAPIPatch)
}
assert.False(t, capturedOpts.ServerSideApply)
assert.False(t, capturedOpts.ForceConflicts)
})
}
})
t.Run("serverSideApply=true sets ServerSideApply=true and ForceConflicts=true", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
strategy cmdutil.DryRunStrategy
expectedError string
}{
{"DryRunNone", cmdutil.DryRunNone, ""},
{"DryRunClient", cmdutil.DryRunClient, "error validating options: --dry-run=client doesn't work with --server-side"},
{"DryRunServer", cmdutil.DryRunServer, ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
var capturedOpts *apply.ApplyOptions
if tc.expectedError == "" {
cmdMocks.On("Apply", mock.Anything).Run(func(args mock.Arguments) {
capturedOpts = args[0].(*apply.ApplyOptions)
}).Return(nil)
}
ssa := true
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, tc.strategy, false, false, ssa, "test-manager")
if tc.expectedError == "" {
require.NoError(t, err)
assert.True(t, capturedOpts.ServerSideApply)
assert.True(t, capturedOpts.ForceConflicts)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError)
}
})
}
})
t.Run("force=true sets DeleteOptions.ForceDeletion", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
strategy cmdutil.DryRunStrategy
}{
{"DryRunNone", cmdutil.DryRunNone},
{"DryRunClient", cmdutil.DryRunClient},
{"DryRunServer", cmdutil.DryRunServer},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
var capturedOpts *apply.ApplyOptions
cmdMocks.On("Apply", mock.Anything).Run(func(args mock.Arguments) {
capturedOpts = args[0].(*apply.ApplyOptions)
}).Return(nil)
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunNone, true, false, false, "")
require.NoError(t, err)
assert.True(t, capturedOpts.DeleteOptions.ForceDeletion)
})
}
})
t.Run("outputModeJSON returns JSONPrinter", func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
k.outputMode = outputModeJSON
var capturedOpts *apply.ApplyOptions
cmdMocks.On("Apply", mock.Anything).Run(func(args mock.Arguments) {
capturedOpts = args[0].(*apply.ApplyOptions)
}).Return(nil)
obj := testingutils.NewPod()
_, err := k.ApplyResource(t.Context(), obj, cmdutil.DryRunServer, false, false, true, "test-manager")
require.NoError(t, err)
// Call ToPrinter and verify it returns a JSON printer
printer, err := capturedOpts.ToPrinter("configured")
require.NoError(t, err)
assert.NotNil(t, printer)
// Verify it's a JSONPrinter by checking the type
_, isJSONPrinter := printer.(*printers.JSONPrinter)
assert.True(t, isJSONPrinter, "Expected printer to be of type *printers.JSONPrinter")
// Verify ShowManagedFields is set to true for JSON output
assert.True(t, capturedOpts.PrintFlags.JSONYamlPrintFlags.ShowManagedFields)
})
}
func TestCreateOptionsConfiguration(t *testing.T) {
t.Parallel()
// Test that newCreateOptions correctly configures all CreateOptions fields
t.Run("general options are correctly set", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
strategy cmdutil.DryRunStrategy
}{
{"DryRunNone", cmdutil.DryRunNone},
{"DryRunClient", cmdutil.DryRunClient},
{"DryRunServer", cmdutil.DryRunServer},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
var capturedOpts *create.CreateOptions
cmdMocks.On("Create", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
capturedOpts = args[0].(*create.CreateOptions)
}).Return(nil)
obj := testingutils.NewPod()
_, err := k.CreateResource(t.Context(), obj, tc.strategy, false)
require.NoError(t, err)
assert.Equal(t, tc.strategy, capturedOpts.DryRunStrategy)
assert.NotEmpty(t, capturedOpts.FilenameOptions.Filenames)
assert.NotNil(t, capturedOpts.PrintObj)
})
}
})
}
func TestReplaceOptionsConfiguration(t *testing.T) {
t.Parallel()
// Test that newReplaceOptions correctly configures all ReplaceOptions fields
t.Run("general options are correctly set", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
strategy cmdutil.DryRunStrategy
}{
{"DryRunNone", cmdutil.DryRunNone},
{"DryRunClient", cmdutil.DryRunClient},
{"DryRunServer", cmdutil.DryRunServer},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
var capturedOpts *replace.ReplaceOptions
cmdMocks.On("Replace", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
capturedOpts = args[0].(*replace.ReplaceOptions)
}).Return(nil)
obj := testingutils.NewPod()
obj.SetNamespace("test-namespace")
_, err := k.ReplaceResource(t.Context(), obj, tc.strategy, false)
require.NoError(t, err)
assert.Equal(t, tc.strategy, capturedOpts.DryRunStrategy)
assert.False(t, capturedOpts.DeleteOptions.ForceDeletion)
assert.NotEmpty(t, capturedOpts.DeleteOptions.Filenames)
assert.Equal(t, "test-namespace", capturedOpts.Namespace)
assert.NotNil(t, capturedOpts.PrintObj)
})
}
})
t.Run("force=true sets DeleteOptions.ForceDeletion correctly", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
strategy cmdutil.DryRunStrategy
expected bool
}{
{"DryRunNone", cmdutil.DryRunNone, true},
{"DryRunClient", cmdutil.DryRunClient, false},
{"DryRunServer", cmdutil.DryRunServer, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
k, cmdMocks := newTestKubectlResourceOperations(t)
var capturedOpts *replace.ReplaceOptions
cmdMocks.On("Replace", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
capturedOpts = args[0].(*replace.ReplaceOptions)
}).Return(nil)
obj := testingutils.NewPod()
_, err := k.ReplaceResource(t.Context(), obj, tc.strategy, true)
require.NoError(t, err)
assert.Equal(t, tc.expected, capturedOpts.DeleteOptions.ForceDeletion)
})
}
})
}