Pass CRDs on createion of kube instance

This commit is contained in:
Volodymyr Zotov
2025-08-27 11:19:17 -05:00
parent a1cbd40f9e
commit f7f5462133
2 changed files with 106 additions and 14 deletions

View File

@@ -13,14 +13,22 @@ import (
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
apiv1 "github.com/1Password/onepassword-operator/api/v1"
"github.com/1Password/onepassword-operator/pkg/testhelper/defaults"
)
type TestConfig struct {
@@ -32,11 +40,13 @@ type Config struct {
Namespace string
ManifestsDir string
TestConfig *TestConfig
CRDs []string
}
type Kube struct {
Config *Config
Client client.Client
Mapper meta.RESTMapper
}
func NewKubeClient(config *Config) *Kube {
@@ -49,12 +59,26 @@ func NewKubeClient(config *Config) *Kube {
restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
Expect(err).NotTo(HaveOccurred())
// Install CRDs first (so discovery sees them)
installCRDs(context.Background(), restConfig, config.CRDs)
// Build an http.Client from restConfig
httpClient, err := rest.HTTPClientFor(restConfig)
Expect(err).NotTo(HaveOccurred())
// Create a Dynamic RESTMapper that uses restConfig
rm, err := apiutil.NewDynamicRESTMapper(restConfig, httpClient)
Expect(err).NotTo(HaveOccurred())
scheme := runtime.NewScheme()
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(appsv1.AddToScheme(scheme))
utilruntime.Must(apiv1.AddToScheme(scheme))
utilruntime.Must(apiv1.AddToScheme(scheme)) // add OnePasswordItem to scheme
kubernetesClient, err := client.New(restConfig, client.Options{Scheme: scheme})
kubernetesClient, err := client.New(restConfig, client.Options{
Scheme: scheme,
Mapper: rm,
})
Expect(err).NotTo(HaveOccurred())
// update the current contexts namespace in kubeconfig
@@ -75,6 +99,7 @@ func NewKubeClient(config *Config) *Kube {
return &Kube{
Config: config,
Client: kubernetesClient,
Mapper: rm,
}
}
@@ -113,19 +138,79 @@ func (k *Kube) ApplyOnePasswordItem(ctx context.Context, fileName string) {
data, err := os.ReadFile(k.Config.ManifestsDir + "/" + fileName)
Expect(err).NotTo(HaveOccurred())
item := &apiv1.OnePasswordItem{}
err = yaml.Unmarshal(data, item)
// Decode YAML -> JSON -> unstructured.Unstructured
jsonBytes, err := yaml.ToJSON(data)
Expect(err).NotTo(HaveOccurred())
if item.Namespace == "" {
item.Namespace = k.Config.Namespace
var obj unstructured.Unstructured
Expect(obj.UnmarshalJSON(jsonBytes)).To(Succeed())
// Default namespace for namespaced resources if not set in YAML
if obj.GetNamespace() == "" && k.Config.Namespace != "" {
gvk := obj.GroupVersionKind()
mapping, mapErr := k.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if mapErr == nil && mapping.Scope.Name() == meta.RESTScopeNameNamespace {
obj.SetNamespace(k.Config.Namespace)
}
}
err = k.Client.Get(c, client.ObjectKey{Name: item.Name, Namespace: k.Config.Namespace}, item)
if errors.IsNotFound(err) {
err = k.Client.Create(c, item)
} else {
err = k.Client.Update(c, item)
// Server-Side Apply (create or update)
patchOpts := []client.PatchOption{
client.FieldOwner("onepassword-e2e"),
client.ForceOwnership, // to force-take conflicting fields
}
Expect(err).NotTo(HaveOccurred())
Expect(k.Client.Patch(c, &obj, client.Apply, patchOpts...)).To(Succeed())
}
func installCRDs(ctx context.Context, restConfig *rest.Config, crdFiles []string) {
apixClient, err := apix.NewForConfig(restConfig)
Expect(err).NotTo(HaveOccurred())
for _, f := range crdFiles {
By("Installing CRD " + f)
b, err := os.ReadFile(filepath.Clean(f))
Expect(err).NotTo(HaveOccurred())
var crd apiextv1.CustomResourceDefinition
err = yaml.Unmarshal(b, &crd)
Expect(err).NotTo(HaveOccurred())
// Create or Update
_, err = apixClient.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, &crd, metav1.CreateOptions{})
if apierrors.IsAlreadyExists(err) {
existing, getErr := apixClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Name, metav1.GetOptions{})
Expect(getErr).NotTo(HaveOccurred())
crd.ResourceVersion = existing.ResourceVersion
_, err = apixClient.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, &crd, metav1.UpdateOptions{})
}
Expect(err).NotTo(HaveOccurred())
waitCRDEstablished(ctx, apixClient, crd.Name)
}
}
// waitCRDEstablished Wait until the CRD reaches Established=True, retrying until the suite timeout.
func waitCRDEstablished(ctx context.Context, apixClient *apix.Clientset, name string) {
By("Waiting for CRD " + name + " to be Established")
Eventually(func(g Gomega) {
// Short per-attempt timeout so a single Get can't hang the whole Eventually loop.
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
crd, err := apixClient.ApiextensionsV1().
CustomResourceDefinitions().
Get(attemptCtx, name, metav1.GetOptions{})
g.Expect(err).NotTo(HaveOccurred())
established := false
for _, c := range crd.Status.Conditions {
if c.Type == apiextv1.Established && c.Status == apiextv1.ConditionTrue {
established = true
break
}
}
g.Expect(established).To(BeTrue(), "CRD %q is not Established yet", name)
}, defaults.E2ETimeout, defaults.E2EInterval).Should(Succeed())
}