Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .wwhrd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ allowlist:
- BSD-2-Clause-FreeBSD
- BSD-3-Clause
- ISC

exceptions:
- github.com/hashicorp/golang-lru/v2 # MPL 2.0
- github.com/hashicorp/golang-lru/v2/internal # MPL 2.0
- github.com/hashicorp/golang-lru/v2/simplelru # MPL 2.0
6 changes: 3 additions & 3 deletions cmd/init-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error {
return fmt.Errorf("failed to setup source factory: %w", err)
}

// clusterApplier controls how the manifests of an init source are applied in
// manifestApplier controls how the manifests of an init source are applied in
// the target workspace
clusterApplier := manifest.NewClusterApplier(clusterClient)
manifestApplier := manifest.NewApplier()

// create the ctrl-runtime manager
mgr, err := setupManager(ctx, cfg, opts)
Expand All @@ -119,7 +119,7 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error {
// wrap this controller creation in a closure to prevent giving all the initcontroller
// dependencies to the targetcontroller
newInitController := func(remoteManager mcmanager.Manager, targetProvider initcontroller.InitTargetProvider, initializer kcpcorev1alpha1.LogicalClusterInitializer) error {
return initcontroller.Create(remoteManager, targetProvider, sourceFactory, clusterApplier, initializer, log, numInitWorkers)
return initcontroller.Create(remoteManager, targetProvider, sourceFactory, manifestApplier, initializer, log, numInitWorkers)
}

if err := targetcontroller.Add(ctx, mgr, log, opts.InitTargetSelector, clusterClient, newInitController); err != nil {
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ replace github.com/kcp-dev/init-agent/sdk => ./sdk

require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
github.com/kcp-dev/init-agent/sdk v0.0.0-00010101000000-000000000000
github.com/kcp-dev/logicalcluster/v3 v3.0.5
Expand All @@ -14,6 +15,7 @@ require (
github.com/spf13/pflag v1.0.10
go.uber.org/zap v1.27.1
k8s.io/api v0.34.2
k8s.io/apiextensions-apiserver v0.34.2
k8s.io/apimachinery v0.34.2
k8s.io/client-go v0.34.2
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
Expand All @@ -32,7 +34,6 @@ require (
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.22.0 // indirect
github.com/go-openapi/jsonreference v0.21.1 // indirect
github.com/go-openapi/swag v0.24.1 // indirect
Expand All @@ -52,6 +53,7 @@ require (
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down Expand Up @@ -92,7 +94,6 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.34.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down
81 changes: 81 additions & 0 deletions hack/ci/run-e2e-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash

# Copyright 2026 The kcp Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -euo pipefail
source hack/lib.sh

# have a place to store things
if [ -z "${ARTIFACTS:-}" ]; then
ARTIFACTS=.e2e/artifacts
mkdir -p "$ARTIFACTS"
fi

echodate "Build artifacts will be placed in $ARTIFACTS."
export ARTIFACTS="$(realpath "$ARTIFACTS")"

# build the agent, we will start it many times during the tests
echodate "Building the init-agent…"
make build

# start a shared kcp process
KCP="$(UGET_PRINT_PATH=relative make --no-print-directory install-kcp)"

KCP_ROOT_DIRECTORY=.kcp.e2e
KCP_LOGFILE="$ARTIFACTS/kcp.log"
KCP_TOKENFILE=hack/ci/testdata/e2e-kcp.tokens

echodate "Starting kcp…"
rm -rf "$KCP_ROOT_DIRECTORY" "$KCP_LOGFILE"
"$KCP" start \
-v4 \
--token-auth-file "$KCP_TOKENFILE" \
--root-directory "$KCP_ROOT_DIRECTORY" 1>"$KCP_LOGFILE" 2>&1 &

stop_kcp() {
echodate "Stopping kcp processes (set \$KEEP_KCP=true to not do this)…"
pkill -e kcp
}

if [[ -v KEEP_KCP ]] && $KEEP_KCP; then
echodate "\$KEEP_KCP is set, will not stop kcp once the script is finished."
else
append_trap stop_kcp EXIT
fi

# make the token available to the Go tests
export KCP_AGENT_TOKEN="$(grep e2e "$KCP_TOKENFILE" | cut -f1 -d,)"

# Wait for kcp to be ready; this env name is also hardcoded in the Go tests.
export KCP_KUBECONFIG="$KCP_ROOT_DIRECTORY/admin.kubeconfig"

# the tenancy API becomes available pretty late during startup, so it's a good readiness check
KUBECTL="$(UGET_PRINT_PATH=relative make --no-print-directory install-kubectl)"
if ! retry_linear 3 20 "$KUBECTL" --kubeconfig "$KCP_KUBECONFIG" get workspaces; then
echodate "kcp never became ready."
exit 1
fi

# makes it easier to reference these files from various _test.go files.
export ROOT_DIRECTORY="$(realpath .)"
export KCP_KUBECONFIG="$(realpath "$KCP_KUBECONFIG")"
export AGENT_BINARY="$(realpath _build/init-agent)"

# time to run the tests
echodate "Running e2e tests…"
WHAT="${WHAT:-./test/e2e/...}"
(set -x; go test -tags e2e -timeout 2h -v $WHAT)

echodate "Done. :-)"
1 change: 1 addition & 0 deletions hack/ci/testdata/e2e-kcp.tokens
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
topphemmelig,init-agent-e2e,1111-2222-3333-4444,"init-agents"
83 changes: 83 additions & 0 deletions hack/run-e2e-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash

# Copyright 2026 The kcp Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -euo pipefail

cd "$(dirname $0)/.."

source hack/lib.sh

export ARTIFACTS=.e2e

rm -rf "$ARTIFACTS"
mkdir -p "$ARTIFACTS"

KCP_ROOT_DIRECTORY="$ARTIFACTS/kcp"
KCP_LOGFILE="$ARTIFACTS/kcp.log"
KCP_TOKENFILE=hack/ci/testdata/e2e-kcp.tokens
KCP_PID=0

echodate "Starting kcp…"
KCP="$(UGET_PRINT_PATH=relative make --no-print-directory install-kcp)"

rm -rf "$KCP_ROOT_DIRECTORY" "$KCP_LOGFILE"
"$KCP" start \
-v4 \
--token-auth-file "$KCP_TOKENFILE" \
--root-directory "$KCP_ROOT_DIRECTORY" 1>"$KCP_LOGFILE" 2>&1 &
KCP_PID=$!

stop_kcp() {
echodate "Stopping kcp (set \$KEEP_KCP=true to not do this)…"
kill -TERM $KCP_PID
wait $KCP_PID
}

if [[ -v KEEP_KCP ]] && $KEEP_KCP; then
echodate "\$KEEP_KCP is set, will not stop kcp once the script is finished."
else
append_trap stop_kcp EXIT
fi

# make the token available to the Go tests
export KCP_AGENT_TOKEN="$(grep e2e "$KCP_TOKENFILE" | cut -f1 -d,)"

# Wait for kcp to be ready; this env name is also hardcoded in the Go tests.
export KCP_KUBECONFIG="$KCP_ROOT_DIRECTORY/admin.kubeconfig"

# the tenancy API becomes available pretty late during startup, so it's a good readiness check
KUBECTL="$(UGET_PRINT_PATH=relative make --no-print-directory install-kubectl)"
if ! retry_linear 3 20 "$KUBECTL" --kubeconfig "$KCP_KUBECONFIG" get workspaces; then
echodate "kcp never became ready."
exit 1
fi

# makes it easier to reference these files from various _test.go files.
export ROOT_DIRECTORY="$(realpath .)"
export KCP_KUBECONFIG="$(realpath "$KCP_KUBECONFIG")"
export AGENT_BINARY="$(realpath _build/init-agent)"

# The tests require ARTIFACTS to be absolute.
ARTIFACTS="$(realpath "$ARTIFACTS")"

# time to run the tests
echodate "Running e2e tests…"

WHAT="${WHAT:-./test/e2e/...}"
TEST_ARGS="${TEST_ARGS:--timeout 30m -v}"
E2E_PARALLELISM=${E2E_PARALLELISM:-2}

(set -x; go test -tags e2e -parallel $E2E_PARALLELISM $TEST_ARGS "$WHAT")
26 changes: 13 additions & 13 deletions internal/controller/initcontroller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ const (
type InitTargetProvider func(ctx context.Context) (*initializationv1alpha1.InitTarget, error)

type Reconciler struct {
remoteManager mcmanager.Manager
targetProvider InitTargetProvider
log *zap.SugaredLogger
sourceFactory *source.Factory
clusterApplier manifest.ClusterApplier
initializer kcpcorev1alpha1.LogicalClusterInitializer
remoteManager mcmanager.Manager
targetProvider InitTargetProvider
log *zap.SugaredLogger
sourceFactory *source.Factory
manifestApplier manifest.Applier
initializer kcpcorev1alpha1.LogicalClusterInitializer
}

// Create creates a new controller and importantly does *not* add it to the manager,
Expand All @@ -55,7 +55,7 @@ func Create(
remoteManager mcmanager.Manager,
targetProvider InitTargetProvider,
sourceFactory *source.Factory,
clusterApplier manifest.ClusterApplier,
manifestApplier manifest.Applier,
initializer kcpcorev1alpha1.LogicalClusterInitializer,
log *zap.SugaredLogger,
numWorkers int,
Expand All @@ -70,11 +70,11 @@ func Create(
}).
For(&kcpcorev1alpha1.LogicalCluster{}).
Complete(&Reconciler{
remoteManager: remoteManager,
targetProvider: targetProvider,
log: log.Named(ControllerName),
sourceFactory: sourceFactory,
clusterApplier: clusterApplier,
initializer: initializer,
remoteManager: remoteManager,
targetProvider: targetProvider,
log: log.Named(ControllerName),
sourceFactory: sourceFactory,
manifestApplier: manifestApplier,
initializer: initializer,
})
}
7 changes: 1 addition & 6 deletions internal/controller/initcontroller/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,6 @@ func (r *Reconciler) reconcile(ctx context.Context, logger *zap.SugaredLogger, c
return requeue, fmt.Errorf("failed to get InitTarget: %w", err)
}

applier, err := r.clusterApplier.Cluster(initialize.ClusterFromContext(ctx))
if err != nil {
return requeue, fmt.Errorf("failed to construct manifests applier: %w", err)
}

for idx, ref := range target.Spec.Sources {
sourceLog := logger.With("init-target", target.Name, "source-idx", idx)
sourceCtx := log.WithLog(ctx, sourceLog)
Expand All @@ -117,7 +112,7 @@ func (r *Reconciler) reconcile(ctx context.Context, logger *zap.SugaredLogger, c

sourceLog.Debugf("Source yielded %d manifests", len(objects))

srcNeedRequeue, err := applier.Apply(sourceCtx, objects)
srcNeedRequeue, err := r.manifestApplier.Apply(sourceCtx, client, objects)
if err != nil {
return requeue, fmt.Errorf("failed to apply source #%d: %w", idx, err)
}
Expand Down
44 changes: 8 additions & 36 deletions internal/manifest/applier.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,57 +21,29 @@ import (
"errors"
"strings"

"github.com/kcp-dev/init-agent/internal/kcp"
"github.com/kcp-dev/init-agent/internal/log"

"github.com/kcp-dev/logicalcluster/v3"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

type ClusterApplier interface {
Cluster(cluster logicalcluster.Name) (Applier, error)
}

type Applier interface {
Apply(ctx context.Context, objs []*unstructured.Unstructured) (requeue bool, err error)
Apply(ctx context.Context, client ctrlruntimeclient.Client, objs []*unstructured.Unstructured) (requeue bool, err error)
}

type clusterApplier struct {
cc kcp.ClusterClient
}
type applier struct{}

func NewClusterApplier(cc kcp.ClusterClient) ClusterApplier {
return &clusterApplier{cc: cc}
}

func (a *clusterApplier) Cluster(cluster logicalcluster.Name) (Applier, error) {
client, err := a.cc.Cluster(cluster, nil) // using unstructured, no scheme needed
if err != nil {
return nil, err
}

return &applier{client: client}, nil
}

type applier struct {
client ctrlruntimeclient.Client
}

func NewApplier(client ctrlruntimeclient.Client) Applier {
return &applier{
client: client,
}
func NewApplier() Applier {
return &applier{}
}

func (a *applier) Apply(ctx context.Context, objs []*unstructured.Unstructured) (requeue bool, err error) {
func (a *applier) Apply(ctx context.Context, client ctrlruntimeclient.Client, objs []*unstructured.Unstructured) (requeue bool, err error) {
SortObjectsByHierarchy(objs)

for _, object := range objs {
err := a.applyObject(ctx, object)
err := a.applyObject(ctx, client, object)
if err != nil {
if errors.Is(err, &meta.NoKindMatchError{}) {
return true, nil
Expand All @@ -84,7 +56,7 @@ func (a *applier) Apply(ctx context.Context, objs []*unstructured.Unstructured)
return false, nil
}

func (a *applier) applyObject(ctx context.Context, obj *unstructured.Unstructured) error {
func (a *applier) applyObject(ctx context.Context, client ctrlruntimeclient.Client, obj *unstructured.Unstructured) error {
gvk := obj.GroupVersionKind()

key := ctrlruntimeclient.ObjectKeyFromObject(obj).String()
Expand All @@ -94,7 +66,7 @@ func (a *applier) applyObject(ctx context.Context, obj *unstructured.Unstructure
logger := log.FromContext(ctx)
logger.Debugw("Applying object", "obj-key", key, "obj-gvk", gvk)

if err := a.client.Create(ctx, obj); err != nil {
if err := client.Create(ctx, obj); err != nil {
if !apierrors.IsAlreadyExists(err) {
return err
}
Expand Down
Loading