Skip to content

Commit

Permalink
Setup reconciliation testing
Browse files Browse the repository at this point in the history
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
  • Loading branch information
stefanprodan committed May 28, 2024
1 parent 5e6d057 commit ac5bf59
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 104 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: e2e

on:
pull_request:
push:
branches:
- 'main'
- 'release/**'

permissions:
contents: read # for actions/checkout to fetch code

jobs:
kind:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version-file: 'go.mod'
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Kubernetes
uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0
with:
version: v0.23.0
cluster_name: kind
- name: Run controller tests
run: make test
- name: Check if working tree is dirty
run: |
if [[ $(git diff --stat) != '' ]]; then
git --no-pager diff
echo 'run make test and commit changes'
exit 1
fi
- name: Run controller e2e tests
run: make test-e2e
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ RUN go mod download
# Copy the go source
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/controller/ internal/controller/
COPY internal/ internal/

# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
Expand Down
8 changes: 7 additions & 1 deletion api/v1alpha1/fluxinstance_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,21 @@ func (in *FluxInstance) GetInterval() time.Duration {
return interval
}

// +kubebuilder:storageversion
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""

// FluxInstance is the Schema for the fluxinstances API
type FluxInstance struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec FluxInstanceSpec `json:"spec,omitempty"`
Spec FluxInstanceSpec `json:"spec,omitempty"`

// +kubebuilder:default:={"observedGeneration":-1}
Status FluxInstanceStatus `json:"status,omitempty"`
}

Expand Down
14 changes: 13 additions & 1 deletion config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@ spec:
singular: fluxinstance
scope: Namespaced
versions:
- name: v1alpha1
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: FluxInstance is the Schema for the fluxinstances API
Expand Down Expand Up @@ -107,6 +117,8 @@ spec:
type: object
type: object
status:
default:
observedGeneration: -1
description: FluxInstanceStatus defines the observed state of FluxInstance
properties:
conditions:
Expand Down
12 changes: 7 additions & 5 deletions internal/controller/fluxinstance_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (r *FluxInstanceReconciler) reconcile(ctx context.Context,
msg = fmt.Sprintf("Reconciliation finished in %s", time.Since(reconcileStart).String())
conditions.MarkTrue(obj,
meta.ReadyCondition,
meta.SucceededReason,
meta.ReconciliationSucceededReason,
msg)
log.Info(msg, "generation", obj.GetGeneration())
r.EventRecorder.Event(obj, corev1.EventTypeNormal, meta.ReconciliationSucceededReason, msg)
Expand All @@ -109,6 +109,7 @@ func (r *FluxInstanceReconciler) reconcile(ctx context.Context,
return result, nil
}

//nolint:unparam
func (r *FluxInstanceReconciler) finalize(ctx context.Context,
obj *fluxcdv1alpha1.FluxInstance) (ctrl.Result, error) {
reconcileStart := time.Now()
Expand All @@ -118,7 +119,6 @@ func (r *FluxInstanceReconciler) finalize(ctx context.Context,

msg := fmt.Sprintf("Uninstallation completed in %v", time.Since(reconcileStart).String())
log.Info(msg)
r.EventRecorder.Event(obj, corev1.EventTypeNormal, meta.SucceededReason, msg)

// Stop reconciliation as the object is being deleted
return ctrl.Result{}, nil
Expand All @@ -132,11 +132,9 @@ func (r *FluxInstanceReconciler) finalizeStatus(ctx context.Context,
obj.Status.LastHandledReconcileAt = v
}

// Remove the Reconciling condition and update the observed generation
// if the reconciliation was successful.
// Remove the Reconciling condition if the reconciliation was successful.
if conditions.IsTrue(obj, meta.ReadyCondition) {
conditions.Delete(obj, meta.ReconcilingCondition)
obj.Status.ObservedGeneration = obj.Generation
}

// Set the Reconciling reason to ProgressingWithRetry if the
Expand Down Expand Up @@ -167,6 +165,10 @@ func (r *FluxInstanceReconciler) patch(ctx context.Context,
patch.WithFieldOwner(r.StatusManager),
}

if conditions.IsTrue(obj, meta.ReadyCondition) {
patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{})
}

// Patch the object status, conditions and finalizers.
if err := patcher.Patch(ctx, obj, patchOpts...); err != nil {
if !obj.GetDeletionTimestamp().IsZero() {
Expand Down
6 changes: 4 additions & 2 deletions internal/controller/fluxinstance_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func TestFluxInstanceReconciler_Install(t *testing.T) {
err = testEnv.Get(ctx, client.ObjectKeyFromObject(obj), result)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(result.Finalizers).To(ContainElement(fluxcdv1alpha1.Finalizer))
g.Expect(result.Status.ObservedGeneration).To(BeEquivalentTo(-1))

r, err = reconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(obj),
Expand All @@ -63,7 +64,8 @@ func TestFluxInstanceReconciler_Install(t *testing.T) {

logObjectStatus(t, result)
checkInstanceReadiness(g, result)
g.Expect(conditions.GetReason(result, meta.ReadyCondition)).To(BeIdenticalTo(meta.SucceededReason))
g.Expect(conditions.GetReason(result, meta.ReadyCondition)).To(BeIdenticalTo(meta.ReconciliationSucceededReason))
g.Expect(result.Status.ObservedGeneration).To(BeEquivalentTo(result.Generation))

// Check if the instance was scheduled for reconciliation.
resultP := result.DeepCopy()
Expand All @@ -82,7 +84,7 @@ func TestFluxInstanceReconciler_Install(t *testing.T) {

// Check if events were recorded for each step.
events := getEvents(result.Name)
g.Expect(len(events)).To(Equal(3))
g.Expect(events).To(HaveLen(3))
g.Expect(events[0].Reason).To(Equal(meta.ProgressingReason))
g.Expect(events[1].Reason).To(Equal(meta.ReconciliationSucceededReason))
g.Expect(events[2].Reason).To(Equal(meta.ReconciliationSucceededReason))
Expand Down
157 changes: 64 additions & 93 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package e2e
import (
"fmt"
"os/exec"
"strings"
"time"

. "github.com/onsi/ginkgo/v2"
Expand All @@ -15,99 +14,71 @@ import (
"github.com/controlplaneio-fluxcd/fluxcd-operator/test/utils"
)

const namespace = "flux-system"

var _ = Describe("controller", Ordered, func() {
BeforeAll(func() {
var controllerPodName string
var err error

// projectimage stores the name of the image used in the example
var projectimage = "test/fluxcd-operator:v0.0.0-dev.1"

By("building the fluxcd-operator image")
cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage))
_, err = utils.Run(cmd)
ExpectWithOffset(1, err).NotTo(HaveOccurred())

By("loading the the fluxcd-operator image on Kind")
err = utils.LoadImageToKindClusterWithName(projectimage)
ExpectWithOffset(1, err).NotTo(HaveOccurred())

By("deploying fluxcd-operator")
cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage))
_, err = utils.Run(cmd)
ExpectWithOffset(1, err).NotTo(HaveOccurred())

By("validating that the fluxcd-operator pod is running as expected")
verifyControllerUp := func() error {
// Get pod name

cmd = exec.Command("kubectl", "get",
"pods", "-l", "app.kubernetes.io/name=fluxcd-operator",
"-o", "go-template={{ range .items }}"+
"{{ if not .metadata.deletionTimestamp }}"+
"{{ .metadata.name }}"+
"{{ \"\\n\" }}{{ end }}{{ end }}",
"-n", namespace,
)

podOutput, err := utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred())
podNames := utils.GetNonEmptyLines(string(podOutput))
if len(podNames) != 1 {
return fmt.Errorf("expect 1 fluxcd-operator pods running, but got %d", len(podNames))
}
controllerPodName = podNames[0]
ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("fluxcd-operator"))
const (
namespace = "flux-system"
)

// Validate pod status
cmd = exec.Command("kubectl", "get",
"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
"-n", namespace,
)
status, err := utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred())
if string(status) != "Running" {
return fmt.Errorf("fluxcd-operator pod in %s status", status)
}
return nil
// Build the fluxcd-operator image and deploy it to the Kind cluster.
var _ = BeforeSuite(func() {
image := "test/fluxcd-operator:v0.0.0-dev.1"
var controllerPodName string
var err error

By("building the fluxcd-operator image")
cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", image))
_, err = utils.Run(cmd)
ExpectWithOffset(1, err).NotTo(HaveOccurred())

By("loading the fluxcd-operator image on Kind")
err = utils.LoadImageToKindClusterWithName(image)
ExpectWithOffset(1, err).NotTo(HaveOccurred())

By("deploying fluxcd-operator")
cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", image))
_, err = utils.Run(cmd)
ExpectWithOffset(1, err).NotTo(HaveOccurred())

By("validating that the fluxcd-operator pod is running as expected")
verifyControllerUp := func() error {
// Get pod name

cmd = exec.Command("kubectl", "get",
"pods", "-l", "app.kubernetes.io/name=fluxcd-operator",
"-o", "go-template={{ range .items }}"+
"{{ if not .metadata.deletionTimestamp }}"+
"{{ .metadata.name }}"+
"{{ \"\\n\" }}{{ end }}{{ end }}",
"-n", namespace,
)

podOutput, err := utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred())
podNames := utils.GetNonEmptyLines(string(podOutput))
if len(podNames) != 1 {
return fmt.Errorf("expect 1 fluxcd-operator pods running, but got %d", len(podNames))
}
EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed())
})

AfterAll(func() {
By("uninstalling flux")
cmd := exec.Command("kubectl", "delete", "-k", "config/samples", "-n", namespace)
_, _ = utils.Run(cmd)

By("uninstalling fluxcd-operator")
cmd = exec.Command("make", "undeploy")
_, _ = utils.Run(cmd)
})

Context("Operator", func() {
It("should run successfully", func() {
By("validating that FluxInstance reconciles as expected")
verifyFluxInstanceReconcile := func() error {
cmd := exec.Command("kubectl", "apply",
"-k", "config/samples", "-n", namespace,
)
_, err := utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred())
controllerPodName = podNames[0]
ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("fluxcd-operator"))

// Validate pod status
cmd = exec.Command("kubectl", "get",
"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
"-n", namespace,
)
status, err := utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred())
if string(status) != "Running" {
return fmt.Errorf("fluxcd-operator pod in %s status", status)
}
return nil
}
EventuallyWithOffset(1, verifyControllerUp, time.Minute, 5*time.Second).Should(Succeed())
})

cmd = exec.Command("kubectl", "get",
"FluxInstance", "flux", "-o", "yaml",
"-n", namespace,
)
status, err := utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred())
if !strings.Contains(string(status), "Succeeded") {
return fmt.Errorf("FluxInstance failed %s", status)
}
return nil
}
EventuallyWithOffset(1, verifyFluxInstanceReconcile, time.Minute, time.Second).Should(Succeed())
})
})
// Delete the fluxcd-operator CRDs, deployment and namespace.
var _ = AfterSuite(func() {
By("uninstalling fluxcd-operator")
cmd := exec.Command("make", "undeploy")
_, err := utils.Run(cmd)
Expect(err).NotTo(HaveOccurred())
})
47 changes: 47 additions & 0 deletions test/e2e/instance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2024 Stefan Prodan.
// SPDX-License-Identifier: AGPL-3.0

package e2e

import (
"os/exec"
"time"

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

"github.com/controlplaneio-fluxcd/fluxcd-operator/test/utils"
)

var _ = Describe("FluxInstance", Ordered, func() {
Context("installation", func() {
It("should run successfully", func() {
By("reconcile FluxInstance")
verifyFluxInstanceReconcile := func() error {
cmd := exec.Command("kubectl", "apply",
"-k", "config/samples", "-n", namespace,
)
_, err := utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred())

cmd = exec.Command("kubectl", "wait", "FluxInstance/flux", "-n", namespace,
"--for=condition=Ready", "--timeout=10s",
)
_, err = utils.Run(cmd)
ExpectWithOffset(2, err).NotTo(HaveOccurred())
return nil
}
EventuallyWithOffset(1, verifyFluxInstanceReconcile, time.Minute, 3*time.Second).Should(Succeed())
})
})

Context("uninstallation", func() {
It("should run successfully", func() {
By("delete FluxInstance")
cmd := exec.Command("kubectl", "delete", "-k", "config/samples",
"--timeout=30s", "-n", namespace)
_, err := utils.Run(cmd)
Expect(err).NotTo(HaveOccurred())
})
})
})
Loading

0 comments on commit ac5bf59

Please sign in to comment.