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 @@ -93,6 +93,19 @@ jobs:
run: make create-cluster fv-agentless
env:
FV: true
FV_OIDC:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: 1.26.6
- name: fv-oidc
run: make create-cluster create-cluster-oidc fv-oidc
env:
FV: true
FV_PULLMODE:
runs-on: ubuntu-latest
steps:
Expand Down
42 changes: 42 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ TIMEOUT ?= 10m
WORKLOAD_CLUSTER_YAML ?= test/$(WORKLOAD_CLUSTER_NAME).yaml
NUM_NODES ?= 8

# Used by create-cluster-oidc / fv-oidc. OIDC_CLIENT_SECRET is a test fixture, not a
# real credential.
DEX_NODE_PORT ?= 30556
OIDC_CLIENT_ID ?= sveltos
OIDC_CLIENT_SECRET ?= sveltos-fv-secret

.PHONY: quickstart
quickstart: ## start kind cluster; install all cluster api components; create a capi cluster; install projectsveltos
$(MAKE) create-control-cluster
Expand Down Expand Up @@ -285,6 +291,42 @@ fv-agentless: $(KUBECTL) $(GINKGO) ## Run Sveltos Controller tests using existin
$(KUBECTL) wait --for=condition=Available deployment/addon-controller -n projectsveltos --timeout=$(TIMEOUT)
cd test/fv; $(GINKGO) -nodes $(NUM_NODES) --label-filter='FV' --v --trace --randomize-all

.PHONY: create-cluster-oidc
create-cluster-oidc: $(KUBECTL) $(ENVSUBST) ## Deploy Dex and register the workload cluster as an OIDC SveltosCluster. Run once, right after create-cluster (not safe to re-run against the same cluster: it appends a patch to the ClusterClass).
@echo "Deploying Dex"
set -euo pipefail; \
DEX_NODE_IP=$$(docker inspect $(CONTROL_CLUSTER_NAME)-control-plane --format '{{.NetworkSettings.Networks.kind.IPAddress}}'); \
DEX_ISSUER_URL="https://$$DEX_NODE_IP:$(DEX_NODE_PORT)/dex"; \
export DEX_NODE_IP DEX_NODE_PORT="$(DEX_NODE_PORT)" DEX_ISSUER_URL OIDC_CLIENT_ID="$(OIDC_CLIENT_ID)" OIDC_CLIENT_SECRET="$(OIDC_CLIENT_SECRET)"; \
$(ENVSUBST) < test/oidc/dex.yaml | $(KUBECTL) apply -f -; \
$(KUBECTL) wait --for=condition=Ready certificate/dex-tls -n dex --timeout=60s; \
$(KUBECTL) rollout status deployment/dex -n dex --timeout=90s; \
\
echo "Trusting Dex as an OIDC issuer on the workload cluster's kube-apiserver"; \
DEX_CA_B64=$$($(KUBECTL) get secret dex-tls -n dex -o jsonpath='{.data.tls\.crt}'); \
export DEX_CA_B64; \
$(ENVSUBST) < test/oidc/clusterclass-patch.json > test/oidc/clusterclass-patch.json.tmp; \
$(KUBECTL) patch clusterclass quick-start -n default --type json --patch-file test/oidc/clusterclass-patch.json.tmp; \
rm -f test/oidc/clusterclass-patch.json.tmp; \
\
echo "Waiting for the workload cluster's control plane to roll out with the OIDC trust"; \
KCP=$$($(KUBECTL) get kubeadmcontrolplane -n default -l cluster.x-k8s.io/cluster-name=$(WORKLOAD_CLUSTER_NAME) -o jsonpath='{.items[0].metadata.name}'); \
sleep 15; \
$(KUBECTL) wait kubeadmcontrolplane $$KCP -n default --for='condition=RollingOut=False' --timeout=$(TIMEOUT); \
\
echo "Granting the OIDC identity cluster-admin on the workload cluster"; \
OIDC_CLIENT_ID="$(OIDC_CLIENT_ID)" $(ENVSUBST) < test/oidc/workload-rbac.yaml | $(KUBECTL) --kubeconfig=./test/fv/workload_kubeconfig apply -f -; \
\
echo "Registering the workload cluster as an OIDC SveltosCluster"; \
WORKLOAD_ENDPOINT="https://$$($(KUBECTL) get cluster $(WORKLOAD_CLUSTER_NAME) -n default -o jsonpath='{.spec.controlPlaneEndpoint.host}:{.spec.controlPlaneEndpoint.port}')"; \
WORKLOAD_APISERVER_CA_B64=$$($(KUBECTL) get secret $(WORKLOAD_CLUSTER_NAME)-ca -n default -o jsonpath='{.data.tls\.crt}'); \
export WORKLOAD_ENDPOINT WORKLOAD_APISERVER_CA_B64; \
$(ENVSUBST) < test/oidc/sveltoscluster.yaml | $(KUBECTL) apply -f -

.PHONY: fv-oidc
fv-oidc: $(KUBECTL) $(GINKGO) ## Run the OIDC workload identity FV test. Run after create-cluster and create-cluster-oidc.
cd test/fv; $(GINKGO) -nodes 1 --label-filter='OIDC' --v --trace

.PHONY: create-cluster-infra
create-cluster-infra: $(KIND) $(CLUSTERCTL) $(KUBECTL) ## Create cluster infrastructure without deploying Sveltos
$(MAKE) create-control-cluster
Expand Down
5 changes: 3 additions & 2 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion controllers/handlers_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func instantiateTemplate(referencedObject client.Object, logger logr.Logger) boo
annotations := referencedObject.GetAnnotations()
if annotations != nil {
if _, ok := annotations[libsveltosv1beta1.PolicyTemplateAnnotation]; ok {
logger.V(logs.LogInfo).Info(fmt.Sprintf("referencedObject %s %s/%s is a template",
logger.V(logs.LogDebug).Info(fmt.Sprintf("referencedObject %s %s/%s is a template",
referencedObject.GetObjectKind().GroupVersionKind().Kind, referencedObject.GetNamespace(), referencedObject.GetName()))
return true
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/onsi/gomega v1.43.0
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1
github.com/projectsveltos/libsveltos v1.14.1-0.20260905063009-a8a9f4f0cfd4
github.com/projectsveltos/libsveltos v1.14.1-0.20260906152235-8d04320142da
github.com/prometheus/client_golang v1.24.1
github.com/sigstore/cosign/v3 v3.1.3
github.com/sigstore/sigstore v1.10.9
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -641,8 +641,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/projectsveltos/libsveltos v1.14.1-0.20260905063009-a8a9f4f0cfd4 h1:A601K466dqM3yWv+U+BtFGPWHjbyAUo7p0iylNg+B74=
github.com/projectsveltos/libsveltos v1.14.1-0.20260905063009-a8a9f4f0cfd4/go.mod h1:U6iGj5KoC/PcTD2vh3XU6gy7g11suThT6sZSEpmLEkU=
github.com/projectsveltos/libsveltos v1.14.1-0.20260906152235-8d04320142da h1:UZpRfT1NHsnpojbGdhP2fuca+iqZYbHIqAw7qbOmneY=
github.com/projectsveltos/libsveltos v1.14.1-0.20260906152235-8d04320142da/go.mod h1:U6iGj5KoC/PcTD2vh3XU6gy7g11suThT6sZSEpmLEkU=
github.com/projectsveltos/lua-utils/glua-json v0.0.0-20251212200258-2b3cdcb7c0f5 h1:khnc+994UszxZYu69J+R5FKiLA/Nk1JQj0EYAkwTWz0=
github.com/projectsveltos/lua-utils/glua-json v0.0.0-20251212200258-2b3cdcb7c0f5/go.mod h1:yVL8KQFa9tmcxgwl9nwIMtKgtmIVC1zaFRSCfOwYvPY=
github.com/projectsveltos/lua-utils/glua-runes v0.0.0-20251212200258-2b3cdcb7c0f5 h1:YbsebwRwTRhV8QacvEAdFqxcxHdeu7JTVtsBovbkgos=
Expand Down
112 changes: 112 additions & 0 deletions test/fv/oidc_workload_identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
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.
*/

package fv_test

import (
"context"
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1"
"github.com/projectsveltos/addon-controller/lib/clusterops"
libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1"
)

// The SveltosCluster and Secrets this test targets are created by `make create-cluster-oidc`,
// against the same physical workload cluster `make create-cluster` provisions: Dex is deployed
// as the IdP, the workload cluster's kube-apiserver is configured to trust it, and the cluster
// is registered a second time as a plain SveltosCluster using OIDC workload identity instead of
// a kubeconfig Secret, mirroring how an on-prem cluster fronted by an enterprise IdP (Dex,
// Keycloak, Okta) would be onboarded.
const (
oidcWorkloadClusterNamespace = "oidc-test"
oidcWorkloadClusterName = "oidc-workload"
)

var _ = Describe("OIDC workload identity", Serial, func() {
const namePrefix = "oidc-"

It("deploys a ClusterProfile to a cluster registered via OIDC workload identity", Label("OIDC"), func() {
Byf("Verifying SveltosCluster %s/%s is ready", oidcWorkloadClusterNamespace, oidcWorkloadClusterName)
Eventually(func() bool {
sveltosCluster := &libsveltosv1beta1.SveltosCluster{}
err := k8sClient.Get(context.TODO(),
types.NamespacedName{Namespace: oidcWorkloadClusterNamespace, Name: oidcWorkloadClusterName},
sveltosCluster)
return err == nil && sveltosCluster.Status.Ready
}, timeout, pollingInterval).Should(BeTrue())

devNamespaceName := randomString()
Byf("Create a ConfigMap with a Namespace to deploy")
configMap := createConfigMapWithPolicy(defaultNamespace, namePrefix+randomString(),
fmt.Sprintf(devNamespace, devNamespaceName))
Expect(k8sClient.Create(context.TODO(), configMap)).To(Succeed())

Byf("Create a ClusterProfile targeting the OIDC SveltosCluster directly")
clusterProfile := &configv1beta1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: namePrefix + randomString(),
},
Spec: configv1beta1.Spec{
ClusterRefs: []corev1.ObjectReference{
{
APIVersion: libsveltosv1beta1.GroupVersion.String(),
Kind: libsveltosv1beta1.SveltosClusterKind,
Namespace: oidcWorkloadClusterNamespace,
Name: oidcWorkloadClusterName,
},
},
SyncMode: configv1beta1.SyncModeContinuous,
PolicyRefs: []configv1beta1.PolicyRef{
{
Kind: string(libsveltosv1beta1.ConfigMapReferencedResourceKind),
Namespace: configMap.Namespace,
Name: configMap.Name,
},
},
},
}
Expect(k8sClient.Create(context.TODO(), clusterProfile)).To(Succeed())

clusterSummary := verifyClusterSummary(clusterops.ClusterProfileLabelName, clusterProfile.Name,
&clusterProfile.Spec, oidcWorkloadClusterNamespace, oidcWorkloadClusterName,
string(libsveltosv1beta1.ClusterTypeSveltos))

Byf("Verifying ClusterSummary %s status is set to Provisioned for Resources feature", clusterSummary.Name)
verifyFeatureStatusIsProvisioned(oidcWorkloadClusterNamespace, clusterSummary.Name, libsveltosv1beta1.FeatureResources)

Byf("Getting client to access the workload cluster")
workloadClient, err := getKindWorkloadClusterKubeconfig()
Expect(err).To(BeNil())
Expect(workloadClient).ToNot(BeNil())

Byf("Verifying Namespace %s was created in the workload cluster via the OIDC access token",
devNamespaceName)
Eventually(func() error {
currentNamespace := &corev1.Namespace{}
return workloadClient.Get(context.TODO(), types.NamespacedName{Name: devNamespaceName}, currentNamespace)
}, timeout, pollingInterval).Should(BeNil())

deleteClusterProfile(clusterProfile)
})
})
69 changes: 69 additions & 0 deletions test/oidc/clusterclass-patch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
[
{
"op": "add",
"path": "/spec/patches/-",
"value": {
"name": "dexOIDC",
"description": "Trusts the FV Dex instance as an OIDC issuer, for the OIDC workload identity FV test.",
"definitions": [
{
"selector": {
"apiVersion": "controlplane.cluster.x-k8s.io/v1beta2",
"kind": "KubeadmControlPlaneTemplate",
"matchResources": {
"controlPlane": true
}
},
"jsonPatches": [
{
"op": "add",
"path": "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-",
"value": {"name": "oidc-issuer-url", "value": "${DEX_ISSUER_URL}"}
},
{
"op": "add",
"path": "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-",
"value": {"name": "oidc-client-id", "value": "${OIDC_CLIENT_ID}"}
},
{
"op": "add",
"path": "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-",
"value": {"name": "oidc-username-claim", "value": "aud"}
},
{
"op": "add",
"path": "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-",
"value": {"name": "oidc-username-prefix", "value": "-"}
},
{
"op": "add",
"path": "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs/-",
"value": {"name": "oidc-ca-file", "value": "/etc/kubernetes/pki/dex-ca.crt"}
},
{
"op": "add",
"path": "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraVolumes/-",
"value": {
"name": "dex-ca",
"hostPath": "/etc/kubernetes/pki/dex-ca.crt",
"mountPath": "/etc/kubernetes/pki/dex-ca.crt",
"pathType": "File",
"readOnly": true
}
},
{
"op": "add",
"path": "/spec/template/spec/kubeadmConfigSpec/files/-",
"value": {
"path": "/etc/kubernetes/pki/dex-ca.crt",
"permissions": "0644",
"encoding": "base64",
"content": "${DEX_CA_B64}"
}
}
]
}
]
}
}
]
Loading