1
0
Fork 0
mirror of https://github.com/imjasonh/git-k8s synced 2026-07-18 15:05:45 +00:00

Fix multiple code review issues: dedup splitKey, fix merge bug, remove dead code

- Extract triplicated splitKey into shared pkg/reconciler/internal package
  using strings.Cut and removing the unnecessary error return
- Fix resolver merge bug: attemptMerge was using treeA.Hash as the merge
  commit tree, silently dropping all of branch B's changes. Now builds a
  proper merged tree that applies B's non-conflicting changes onto A's tree
- Replace hand-rolled nestedFieldNoCopy/unstructuredNestedStringMap with
  k8s.io/apimachinery's unstructured.NestedStringMap stdlib equivalent
- Hoist branch listing out of per-refspec loop in updateBranches to avoid
  redundant API calls
- Add not-found handling in all three reconcilers so deleted resources
  don't cause infinite requeue loops
- Fix client.Get() to use checked type assertion with a clear panic message
  instead of an opaque nil-pointer dereference
- Remove dead code: unused addDefaultingFuncs wrapper, unused context
  injection in push controller setup

https://claude.ai/code/session_01634JRpHEoM9FzuBjZ7qHcY
This commit is contained in:
Claude 2026-02-28 04:39:00 +00:00
parent 9d5cd83734
commit 0cb9e4aa1d
No known key found for this signature in database
11 changed files with 166 additions and 409 deletions

View file

@ -35,7 +35,6 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl
}
gitClient := gitclient.NewFromDynamic(dynClient)
ctx = gitclient.WithClient(ctx, gitClient)
// Create dynamic informer for GitPushTransaction.
factory := dynamicinformer.NewDynamicSharedInformerFactory(dynClient, 30*time.Second)

View file

@ -11,14 +11,17 @@ import (
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"knative.dev/pkg/logging"
"knative.dev/pkg/reconciler"
gitv1alpha1 "github.com/imjasonh/git-k8s/pkg/apis/git/v1alpha1"
gitclient "github.com/imjasonh/git-k8s/pkg/client"
"github.com/imjasonh/git-k8s/pkg/reconciler/internal"
)
// Reconciler implements the reconcile logic for GitPushTransaction.
@ -31,13 +34,14 @@ type Reconciler struct {
func (r *Reconciler) ReconcileKind(ctx context.Context, key string) reconciler.Event {
logger := logging.FromContext(ctx)
namespace, name, err := splitKey(key)
if err != nil {
return err
}
namespace, name := internal.SplitKey(key)
// Fetch the GitPushTransaction.
txn, err := r.gitClient.GitPushTransactions(namespace).Get(ctx, name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
logger.Debugf("GitPushTransaction %s/%s no longer exists", namespace, name)
return nil
}
if err != nil {
return fmt.Errorf("getting GitPushTransaction %s/%s: %w", namespace, name, err)
}
@ -158,7 +162,7 @@ func (r *Reconciler) resolveAuth(ctx context.Context, namespace string, repo *gi
return nil, fmt.Errorf("getting secret %q: %w", repo.Spec.Auth.SecretRef.Name, err)
}
data, found, err := unstructuredNestedStringMap(u.Object, "data")
data, found, err := unstructured.NestedStringMap(u.Object, "data")
if err != nil || !found {
return nil, fmt.Errorf("reading secret data: found=%v err=%v", found, err)
}
@ -181,42 +185,6 @@ func (r *Reconciler) resolveAuth(ctx context.Context, namespace string, repo *gi
}, nil
}
// unstructuredNestedStringMap extracts a map[string]string from an unstructured object path.
func unstructuredNestedStringMap(obj map[string]interface{}, fields ...string) (map[string]string, bool, error) {
val, found, err := nestedFieldNoCopy(obj, fields...)
if !found || err != nil {
return nil, found, err
}
m, ok := val.(map[string]interface{})
if !ok {
return nil, false, fmt.Errorf("expected map, got %T", val)
}
result := make(map[string]string, len(m))
for k, v := range m {
s, ok := v.(string)
if !ok {
continue
}
result[k] = s
}
return result, true, nil
}
func nestedFieldNoCopy(obj map[string]interface{}, fields ...string) (interface{}, bool, error) {
var val interface{} = obj
for _, field := range fields {
m, ok := val.(map[string]interface{})
if !ok {
return nil, false, nil
}
val, ok = m[field]
if !ok {
return nil, false, nil
}
}
return val, true, nil
}
// failTransaction marks a GitPushTransaction as Failed with the given message.
func (r *Reconciler) failTransaction(ctx context.Context, txn *gitv1alpha1.GitPushTransaction, message string) error {
logger := logging.FromContext(ctx)
@ -234,13 +202,14 @@ func (r *Reconciler) failTransaction(ctx context.Context, txn *gitv1alpha1.GitPu
// updateBranches updates GitBranch CRs after a successful push.
func (r *Reconciler) updateBranches(ctx context.Context, namespace string, txn *gitv1alpha1.GitPushTransaction) error {
// List branches once rather than per-refspec.
branches, err := r.gitClient.GitBranches(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return fmt.Errorf("listing branches: %w", err)
}
now := metav1.Now()
for _, rs := range txn.Spec.RefSpecs {
// List branches matching the destination ref.
branches, err := r.gitClient.GitBranches(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return fmt.Errorf("listing branches: %w", err)
}
for i := range branches.Items {
branch := &branches.Items[i]
destRef := fmt.Sprintf("refs/heads/%s", branch.Spec.BranchName)
@ -256,16 +225,6 @@ func (r *Reconciler) updateBranches(ctx context.Context, namespace string, txn *
return nil
}
// splitKey splits a namespace/name key.
func splitKey(key string) (string, string, error) {
for i := range key {
if key[i] == '/' {
return key[:i], key[i+1:], nil
}
}
return "", key, nil
}
// Ensure Reconciler implements the reconciler interface.
var _ reconciler.LeaderAware = (*Reconciler)(nil)

View file

@ -5,215 +5,6 @@ import (
"testing"
)
func TestSplitKey(t *testing.T) {
tests := []struct {
key string
wantNS string
wantName string
}{
{
key: "default/my-resource",
wantNS: "default",
wantName: "my-resource",
},
{
key: "kube-system/controller",
wantNS: "kube-system",
wantName: "controller",
},
{
key: "no-namespace",
wantNS: "",
wantName: "no-namespace",
},
{
key: "ns/name/with/slashes",
wantNS: "ns",
wantName: "name/with/slashes",
},
{
key: "",
wantNS: "",
wantName: "",
},
}
for _, tt := range tests {
t.Run(tt.key, func(t *testing.T) {
ns, name, err := splitKey(tt.key)
if err != nil {
t.Fatalf("splitKey(%q) error = %v", tt.key, err)
}
if ns != tt.wantNS {
t.Errorf("namespace = %q, want %q", ns, tt.wantNS)
}
if name != tt.wantName {
t.Errorf("name = %q, want %q", name, tt.wantName)
}
})
}
}
func TestNestedFieldNoCopy(t *testing.T) {
tests := []struct {
name string
obj map[string]interface{}
fields []string
want interface{}
found bool
}{
{
name: "simple field",
obj: map[string]interface{}{
"key": "value",
},
fields: []string{"key"},
want: "value",
found: true,
},
{
name: "nested field",
obj: map[string]interface{}{
"level1": map[string]interface{}{
"level2": "deep-value",
},
},
fields: []string{"level1", "level2"},
want: "deep-value",
found: true,
},
{
name: "missing field",
obj: map[string]interface{}{
"key": "value",
},
fields: []string{"missing"},
want: nil,
found: false,
},
{
name: "non-map intermediate",
obj: map[string]interface{}{
"key": "not-a-map",
},
fields: []string{"key", "subkey"},
want: nil,
found: false,
},
{
name: "empty object",
obj: map[string]interface{}{},
fields: []string{"key"},
want: nil,
found: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, found, err := nestedFieldNoCopy(tt.obj, tt.fields...)
if err != nil {
t.Fatalf("nestedFieldNoCopy() error = %v", err)
}
if found != tt.found {
t.Errorf("found = %v, want %v", found, tt.found)
}
if found && got != tt.want {
t.Errorf("value = %v, want %v", got, tt.want)
}
})
}
}
func TestUnstructuredNestedStringMap(t *testing.T) {
tests := []struct {
name string
obj map[string]interface{}
fields []string
wantMap map[string]string
wantFound bool
wantErr bool
}{
{
name: "valid string map",
obj: map[string]interface{}{
"data": map[string]interface{}{
"username": base64.StdEncoding.EncodeToString([]byte("admin")),
"password": base64.StdEncoding.EncodeToString([]byte("secret")),
},
},
fields: []string{"data"},
wantMap: map[string]string{
"username": base64.StdEncoding.EncodeToString([]byte("admin")),
"password": base64.StdEncoding.EncodeToString([]byte("secret")),
},
wantFound: true,
},
{
name: "missing data field",
obj: map[string]interface{}{
"metadata": map[string]interface{}{},
},
fields: []string{"data"},
wantFound: false,
},
{
name: "non-string values are skipped",
obj: map[string]interface{}{
"data": map[string]interface{}{
"string-key": "string-value",
"int-key": 42,
"bool-key": true,
},
},
fields: []string{"data"},
wantMap: map[string]string{
"string-key": "string-value",
},
wantFound: true,
},
{
name: "not a map type",
obj: map[string]interface{}{
"data": "just-a-string",
},
fields: []string{"data"},
wantErr: true,
},
{
name: "empty map",
obj: map[string]interface{}{
"data": map[string]interface{}{},
},
fields: []string{"data"},
wantMap: map[string]string{},
wantFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, found, err := unstructuredNestedStringMap(tt.obj, tt.fields...)
if (err != nil) != tt.wantErr {
t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
}
if found != tt.wantFound {
t.Errorf("found = %v, want %v", found, tt.wantFound)
}
if tt.wantMap != nil {
if len(got) != len(tt.wantMap) {
t.Errorf("map length = %d, want %d", len(got), len(tt.wantMap))
}
for k, v := range tt.wantMap {
if got[k] != v {
t.Errorf("map[%q] = %q, want %q", k, got[k], v)
}
}
}
})
}
}
func TestBase64Decoding(t *testing.T) {
// Verify that values typically stored in Kubernetes secrets
// (base64-encoded via dynamic client) decode correctly.