Skip to content

Commit

Permalink
feat: be able to import any k8s obj as source
Browse files Browse the repository at this point in the history
This permit any object as source
  • Loading branch information
guilhem committed Dec 22, 2024
1 parent a284bfb commit 94d3d3d
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 33 deletions.
133 changes: 100 additions & 33 deletions internal/controller/kustomization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/discovery"
kuberecorder "k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
Expand Down Expand Up @@ -85,6 +87,7 @@ type KustomizationReconciler struct {
client.Client
kuberecorder.EventRecorder
runtimeCtrl.Metrics
DiscoveryClient discovery.DiscoveryInterface

artifactFetchRetries int
requeueDependency time.Duration
Expand Down Expand Up @@ -520,59 +523,123 @@ func (r *KustomizationReconciler) checkDependencies(ctx context.Context,
return nil
}

type Source struct {
runtime.Object

Artifact *sourcev1.Artifact
Duration time.Duration
}

func (s Source) GetArtifact() *sourcev1.Artifact {
return s.Artifact
}

func (s Source) GetRequeueAfter() time.Duration {
if s.Duration != 0 {
return s.Duration
}
return time.Minute
}

func (r *KustomizationReconciler) getSource(ctx context.Context,
obj *kustomizev1.Kustomization) (sourcev1.Source, error) {
var src sourcev1.Source
srcRef := obj.Spec.SourceRef
sourceNamespace := obj.GetNamespace()
if obj.Spec.SourceRef.Namespace != "" {
sourceNamespace = obj.Spec.SourceRef.Namespace
sourceNamespace = srcRef.Namespace
}
namespacedName := types.NamespacedName{
Namespace: sourceNamespace,
Name: obj.Spec.SourceRef.Name,
Name: srcRef.Name,
}

if r.NoCrossNamespaceRefs && sourceNamespace != obj.GetNamespace() {
return src, acl.AccessDeniedError(
return nil, acl.AccessDeniedError(
fmt.Sprintf("can't access '%s/%s', cross-namespace references have been blocked",
obj.Spec.SourceRef.Kind, namespacedName))
srcRef.Kind, namespacedName))
}

switch obj.Spec.SourceRef.Kind {
case sourcev1b2.OCIRepositoryKind:
var repository sourcev1b2.OCIRepository
err := r.Client.Get(ctx, namespacedName, &repository)
apiVersion := srcRef.APIVersion

if apiVersion == "" {
// Get the server preferred resources to determine the API version.

preferredList, err := r.DiscoveryClient.ServerPreferredResources()
if err != nil {
if apierrors.IsNotFound(err) {
return src, err
}
return src, fmt.Errorf("unable to get source '%s': %w", namespacedName, err)
return nil, fmt.Errorf("failed to get server preferred resources: %w", err)
}
src = &repository
case sourcev1.GitRepositoryKind:
var repository sourcev1.GitRepository
err := r.Client.Get(ctx, namespacedName, &repository)
if err != nil {
if apierrors.IsNotFound(err) {
return src, err

var preferredApiResource *metav1.APIResource
// Check if the source kind is available in the cluster.
for _, list := range preferredList {
for _, resource := range list.APIResources {
if resource.Kind == srcRef.Kind {
preferredApiResource = &resource
break
}
}
return src, fmt.Errorf("unable to get source '%s': %w", namespacedName, err)
}
src = &repository
case sourcev1.BucketKind:
var bucket sourcev1.Bucket
err := r.Client.Get(ctx, namespacedName, &bucket)

gv := schema.GroupVersion{Group: preferredApiResource.Group, Version: preferredApiResource.Version}

apiVersion = gv.String()
}

srcUnstructured := &unstructured.Unstructured{}
srcUnstructured.SetKind(srcRef.Kind)
srcUnstructured.SetAPIVersion(apiVersion)

if err := r.Get(ctx, namespacedName, srcUnstructured); err != nil {
return nil, fmt.Errorf("source '%s' not found: %w", namespacedName, err)
}

// get Requeue Duration from srcUnstructured in Spec.Interval.Duration

spec, ok := srcUnstructured.Object["spec"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("spec is not a map")
}

duration := time.Minute

interval, ok := spec["interval"].(map[string]interface{})
if ok {
durationStr, ok := interval["duration"].(string)
if !ok {
return nil, fmt.Errorf("duration is not a string")
}

d, err := time.ParseDuration(durationStr)
if err != nil {
if apierrors.IsNotFound(err) {
return src, err
}
return src, fmt.Errorf("unable to get source '%s': %w", namespacedName, err)
return nil, fmt.Errorf("failed to parse duration: %w", err)
}
src = &bucket
default:
return src, fmt.Errorf("source `%s` kind '%s' not supported",
obj.Spec.SourceRef.Name, obj.Spec.SourceRef.Kind)

duration = d
}

status, ok := srcUnstructured.Object["status"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("status is not a map")
}

artifact, ok := status["artifact"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("artifact is not a map")
}

artifactObj := &sourcev1.Artifact{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(artifact, artifactObj); err != nil {
return nil, fmt.Errorf("failed to convert artifact: %w", err)
}

src := &Source{
Object: srcUnstructured,
Duration: duration,
Artifact: artifactObj,
}

// Get the source object. with unstructured.Unstructured

return src, nil
}

Expand Down
64 changes: 64 additions & 0 deletions internal/controller/kustomization_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,67 @@ func TestKustomizationReconciler_deleteBeforeFinalizer(t *testing.T) {
_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(kustomization)})
g.Expect(err).NotTo(HaveOccurred())
}

func TestKustomizationReconciler_SourceRefAPIVersion(t *testing.T) {
g := NewWithT(t)

namespaceName := "kust-" + randStringRunes(5)
namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: namespaceName},
}
g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred())
t.Cleanup(func() {
g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred())
})

err := createKubeConfigSecret(namespaceName)
g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")

artifactName := "val-" + randStringRunes(5)
artifactChecksum, err := testServer.ArtifactFromDir("testdata/crds", artifactName)
g.Expect(err).ToNot(HaveOccurred())

repositoryName := types.NamespacedName{
Name: fmt.Sprintf("val-%s", randStringRunes(5)),
Namespace: namespaceName,
}

err = applyGitRepository(repositoryName, artifactName, "main/"+artifactChecksum)
g.Expect(err).NotTo(HaveOccurred())

kustomization := &kustomizev1.Kustomization{}
kustomization.Name = "test-kust"
kustomization.Namespace = namespaceName
kustomization.Spec = kustomizev1.KustomizationSpec{
Interval: metav1.Duration{Duration: 10 * time.Minute},
Prune: true,
Path: "./",
SourceRef: kustomizev1.CrossNamespaceSourceReference{
Name: repositoryName.Name,
Namespace: repositoryName.Namespace,
Kind: sourcev1.GitRepositoryKind,
APIVersion: sourcev1.GroupVersion.String(),
},
KubeConfig: &meta.KubeConfigReference{
SecretRef: meta.SecretKeyReference{
Name: "kubeconfig",
},
},
}

g.Expect(k8sClient.Create(context.Background(), kustomization)).To(Succeed())

g.Eventually(func() bool {
var obj kustomizev1.Kustomization
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &obj)
return isReconcileSuccess(&obj) && obj.Status.LastAttemptedRevision == "main/"+artifactChecksum
}, timeout, time.Second).Should(BeTrue())

g.Expect(k8sClient.Delete(context.Background(), kustomization)).To(Succeed())

g.Eventually(func() bool {
var obj kustomizev1.Kustomization
err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), &obj)
return errors.IsNotFound(err)
}, timeout, time.Second).Should(BeTrue())
}
3 changes: 3 additions & 0 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -173,8 +174,10 @@ func TestMain(m *testing.M) {
// for inspection.
kstatusInProgressCheck = kcheck.NewInProgressChecker(testEnv.Client)
kstatusInProgressCheck.DisableFetch = true

reconciler = &KustomizationReconciler{
ControllerName: controllerName,
DiscoveryClient: discovery.NewDiscoveryClientForConfigOrDie(testEnv.Config),
Client: testEnv,
APIReader: testEnv,
EventRecorder: testEnv.GetEventRecorderFor(controllerName),
Expand Down
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
flag "github.com/spf13/pflag"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/discovery"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
Expand Down Expand Up @@ -234,10 +235,17 @@ func main() {
os.Exit(1)
}

discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig)
if err != nil {
setupLog.Error(err, "unable to create discovery client")
os.Exit(1)
}

if err = (&controller.KustomizationReconciler{
ControllerName: controllerName,
DefaultServiceAccount: defaultServiceAccount,
Client: mgr.GetClient(),
DiscoveryClient: discoveryClient,
APIReader: mgr.GetAPIReader(),
Metrics: metricsH,
EventRecorder: eventRecorder,
Expand Down

0 comments on commit 94d3d3d

Please sign in to comment.