Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: be able to import any k8s obj as source #1321

Closed
wants to merge 1 commit into from
Closed
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
129 changes: 97 additions & 32 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,121 @@ 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 nil, fmt.Errorf("failed to parse duration: %w", err)
}

duration = d
}

var artifactObj *sourcev1.Artifact

status, ok := srcUnstructured.Object["status"].(map[string]interface{})
if ok {
artifact, ok := status["artifact"].(map[string]interface{})
if ok {
artifactObjtmp := &sourcev1.Artifact{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(artifact, artifactObj); err != nil {
return nil, fmt.Errorf("failed to convert artifact: %w", err)
}
return src, fmt.Errorf("unable to get source '%s': %w", namespacedName, err)

artifactObj = artifactObjtmp
}
src = &bucket
default:
return src, fmt.Errorf("source `%s` kind '%s' not supported",
obj.Spec.SourceRef.Name, obj.Spec.SourceRef.Kind)
}

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

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