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
13 changes: 13 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,16 @@ jobs:
run: make create-cluster fv
env:
FV: true
FV-PLUGIN:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.26.3
- name: fv
run: make create-cluster-fv fv-exec-plugin
env:
FV: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ hack/tools/bin/
# Needed by fv
test/fv/workload_kubeconfig
test/fv/production_kubeconfig
test/fv/exec-plugin-linux-amd64
bin/manager

manager_image_patch.yaml-e
Expand Down
44 changes: 44 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,28 @@ K8S_VERSION := v1.35.0
endif

KIND_CONFIG ?= kind-cluster.yaml
WORKLOAD_KIND_CONFIG ?= workload-kind-cluster.yaml
CONTROL_CLUSTER_NAME ?= sveltos-management
WORKLOAD_CLUSTER_NAME ?= fv-workload
SVELTOS_NETWORK_NAME ?= sveltos-kind-network
TIMEOUT ?= 10m
NUM_NODES ?= 1
EXEC_PLUGIN_BINARY ?= test/fv/exec-plugin-linux-amd64

.PHONY: kind-test
kind-test: test create-cluster fv ## Build docker image; start kind cluster; load docker image; run fv

.PHONY: kind-test-exec-plugin
kind-test-exec-plugin: test create-cluster-fv fv-exec-plugin ## Build images; start two kind clusters; run all FV tests including exec-plugin

.PHONY: fv
fv: $(GINKGO) ## Run controller tests using an existing cluster
cd test/fv; $(GINKGO) -nodes $(NUM_NODES) --label-filter='FV' --v --trace --randomize-all

.PHONY: fv-exec-plugin
fv-exec-plugin: $(GINKGO) ## Run exec-plugin FV tests against an existing two-cluster setup
cd test/fv; $(GINKGO) -nodes $(NUM_NODES) --label-filter='FV-EXECPLUGIN' --v --trace --randomize-all

.PHONY: test
test: manifests generate fmt vet $(SETUP_ENVTEST) ## Run unit tests.
KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" go test $(shell go list ./... |grep -v test/fv) $(TEST_ARGS) -coverprofile cover.out
Expand All @@ -160,16 +171,49 @@ create-cluster: $(KIND) $(KUBECTL) $(KUSTOMIZE) $(ENVSUBST) ## Create a new kind
$(MAKE) create-control-cluster
$(MAKE) deploy-projectsveltos

.PHONY: create-cluster-fv
create-cluster-fv: $(KIND) $(KUBECTL) $(KUSTOMIZE) $(ENVSUBST) build-exec-plugin ## Create two kind clusters on a shared network and deploy the controller
docker network rm $(SVELTOS_NETWORK_NAME) 2>/dev/null || true
docker network inspect $(SVELTOS_NETWORK_NAME) > /dev/null 2>&1 || docker network create $(SVELTOS_NETWORK_NAME)

@echo "Create the management cluster"
sed -e "s/K8S_VERSION/$(K8S_VERSION)/g" test/$(KIND_CONFIG) > test/$(KIND_CONFIG).tmp
$(KIND) create cluster --name=$(CONTROL_CLUSTER_NAME) --config test/$(KIND_CONFIG).tmp
$(MAKE) deploy-projectsveltos

@echo "Create the workload cluster"
sed -e "s/K8S_VERSION/$(K8S_VERSION)/g" test/$(WORKLOAD_KIND_CONFIG) > test/$(WORKLOAD_KIND_CONFIG).tmp
$(KIND) create cluster --name=$(WORKLOAD_CLUSTER_NAME) --config test/$(WORKLOAD_KIND_CONFIG).tmp

@echo "Connect both clusters to the shared docker network"
docker network connect $(SVELTOS_NETWORK_NAME) $(CONTROL_CLUSTER_NAME)-control-plane
docker network connect $(SVELTOS_NETWORK_NAME) $(WORKLOAD_CLUSTER_NAME)-control-plane

@echo "Copy exec-plugin binary onto the management cluster node"
docker cp $(EXEC_PLUGIN_BINARY) $(CONTROL_CLUSTER_NAME)-control-plane:/usr/local/bin/test-exec-plugin
docker exec $(CONTROL_CLUSTER_NAME)-control-plane chmod 755 /usr/local/bin/test-exec-plugin

@echo "Save workload cluster kubeconfig for FV tests"
$(KIND) get kubeconfig --name $(WORKLOAD_CLUSTER_NAME) > test/fv/workload_kubeconfig

@echo "Switch back to management cluster context"
$(KUBECTL) config use-context kind-$(CONTROL_CLUSTER_NAME)

.PHONY: delete-cluster
delete-cluster: $(KIND) ## Delete the kind cluster
$(KIND) delete cluster --name $(CONTROL_CLUSTER_NAME)
$(KIND) delete cluster --name $(WORKLOAD_CLUSTER_NAME)

##@ Build

.PHONY: build
build: generate fmt vet ## Build manager binary.
go build -o bin/manager cmd/main.go

.PHONY: build-exec-plugin
build-exec-plugin: ## Build the FV exec-plugin binary for linux/amd64 (injected into the controller pod during exec-plugin FV tests)
GOOS=linux GOARCH=amd64 go build -o $(EXEC_PLUGIN_BINARY) ./test/fv/exec-plugin/

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
go run ./cmd/main.go
Expand Down
24 changes: 22 additions & 2 deletions controllers/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,17 @@ func reconcileKubeconfigSecret(ctx context.Context, c client.Client,
Data: map[string][]byte{kubeconfigKey: kubeconfigData},
}
logger.V(logs.LogDebug).Info("creating kubeconfig secret")
return c.Create(ctx, secret)
if createErr := c.Create(ctx, secret); createErr != nil {
if !apierrors.IsAlreadyExists(createErr) {
return createErr
}
// Race: another reconcile created the secret between our Get and Create.
if err = c.Get(ctx, types.NamespacedName{Namespace: cp.Namespace, Name: secretName}, existing); err != nil {
return fmt.Errorf("failed to re-get kubeconfig secret: %w", err)
}
} else {
return nil
}
}

if bytes.Equal(existing.Data[kubeconfigKey], kubeconfigData) {
Expand Down Expand Up @@ -261,7 +271,17 @@ func reconcileSveltosCluster(ctx context.Context, c client.Client,
},
}
logger.V(logs.LogDebug).Info("creating SveltosCluster")
return c.Create(ctx, sc)
if createErr := c.Create(ctx, sc); createErr != nil {
if !apierrors.IsAlreadyExists(createErr) {
return createErr
}
// Race: another reconcile created the SveltosCluster between our Get and Create.
if err = c.Get(ctx, types.NamespacedName{Namespace: cp.Namespace, Name: cp.Name}, existing); err != nil {
return fmt.Errorf("failed to re-get SveltosCluster: %w", err)
}
} else {
return nil
}
}

if existing.Spec.KubeconfigName == secretName && existing.Spec.KubeconfigKeyName == kubeconfigKey {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ go 1.26.3
require (
github.com/TwiN/go-color v1.4.1
github.com/go-logr/logr v1.4.3
github.com/onsi/ginkgo/v2 v2.28.3
github.com/onsi/gomega v1.40.0
github.com/onsi/ginkgo/v2 v2.29.0
github.com/onsi/gomega v1.41.0
github.com/projectsveltos/libsveltos v1.10.1-0.20260521153750-a1f348424b3f
github.com/spf13/pflag v1.0.10
k8s.io/api v0.36.1
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4=
github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc=
github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
github.com/onsi/ginkgo/v2 v2.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag=
github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA=
github.com/onsi/gomega v1.41.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
16 changes: 13 additions & 3 deletions test/fv/clusterprofile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
"k8s.io/client-go/util/retry"
clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand Down Expand Up @@ -85,9 +86,18 @@ var _ = Describe("ClusterProfile controller", Label("FV"), func() {
cp := buildFvClusterProfile(cpName, namespace)
Expect(k8sClient.Create(context.TODO(), cp)).To(Succeed())

// Status is a sub-resource; update it separately.
cp.Status = buildFvAccessProviderStatus(srcSecretName, "kubeconfig", namespace)
Expect(k8sClient.Status().Update(context.TODO(), cp)).To(Succeed())
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
currentCP := &clusterinventoryv1alpha1.ClusterProfile{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{
Namespace: cp.Namespace, Name: cp.Name},
currentCP)
if err != nil {
return err
}
currentCP.Status = buildFvAccessProviderStatus(srcSecretName, "kubeconfig", namespace)
return k8sClient.Status().Update(context.TODO(), currentCP)
})
Expect(err).To(BeNil())

expectedSecretName := cpName + "-sveltos-kubeconfig"

Expand Down
52 changes: 52 additions & 0 deletions test/fv/exec-plugin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright 2026. projectsveltos.io. All rights reserved.

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.
*/

// exec-plugin is a minimal Kubernetes exec credential plugin used only in FV tests.
// It reads a bearer token from a well-known file and writes an ExecCredential JSON
// to stdout so that the clusterinventory-controller can embed it in a kubeconfig.
package main

import (
"encoding/json"
"fmt"
"os"
"strings"
)

const (
tokenFile = "/var/run/secrets/test-exec-plugin/token" //nolint: gosec // used for testing
)

func main() {
raw, err := os.ReadFile(tokenFile)
if err != nil {
fmt.Fprintf(os.Stderr, "exec-plugin: read token: %v\n", err)
os.Exit(1)
}

cred := map[string]interface{}{
"apiVersion": "client.authentication.k8s.io/v1",
"kind": "ExecCredential",
"status": map[string]interface{}{
"token": strings.TrimSpace(string(raw)),
},
}

if err := json.NewEncoder(os.Stdout).Encode(cred); err != nil {
fmt.Fprintf(os.Stderr, "exec-plugin: encode: %v\n", err)
os.Exit(1)
}
}
Loading