Skip to content

Commit

Permalink
Merge pull request #867 from weaveworks/825-observed-generation
Browse files Browse the repository at this point in the history
Defaults ObservedGeneration to -1 and sets LastHandledReconcileAt
  • Loading branch information
Luiz Filho authored Aug 30, 2023
2 parents a4633fd + aa0706d commit 167ed67
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 13 deletions.
3 changes: 2 additions & 1 deletion api/v1alpha2/terraform_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,8 @@ type Terraform struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec TerraformSpec `json:"spec,omitempty"`
Spec TerraformSpec `json:"spec,omitempty"`
// +kubebuilder:default={"observedGeneration":-1}
Status TerraformStatus `json:"status,omitempty"`
}

Expand Down
2 changes: 2 additions & 0 deletions config/crd/bases/infra.contrib.fluxcd.io_terraforms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9807,6 +9807,8 @@ spec:
- sourceRef
type: object
status:
default:
observedGeneration: -1
description: TerraformStatus defines the observed state of Terraform
properties:
availableOutputs:
Expand Down
7 changes: 4 additions & 3 deletions controllers/tc000010_no_outputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
)

// +kubebuilder:docs-gen:collapse=Imports
Expand All @@ -22,9 +23,9 @@ func Test_000010_no_outputs_test(t *testing.T) {
Spec("This spec describes the behaviour of a Terraform resource with no backend, and `auto` approve.")
It("should be reconciled to have available outputs.")

const (
var (
sourceName = "test-tf-controller-no-output"
terraformName = "helloworld-no-outputs"
terraformName = "helloworld-no-outputs-" + rand.String(6)
)
g := NewWithT(t)
ctx := context.Background()
Expand Down Expand Up @@ -210,7 +211,7 @@ spec:
return err.Error()
}
return tfStateSecret.Name
}, timeout, interval).Should(Equal("secrets \"tfstate-default-helloworld-no-outputs\" not found"))
}, timeout, interval).Should(Equal(fmt.Sprintf("secrets \"tfstate-default-%s\" not found", terraformName)))
} else {
// TODO there's must be the default tfstate secret
}
Expand Down
7 changes: 4 additions & 3 deletions controllers/tc000011_workspace_no_outputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
)

// +kubebuilder:docs-gen:collapse=Imports
Expand All @@ -24,9 +25,9 @@ func Test_000011_workspace_no_outputs_test(t *testing.T) {
Spec("This spec describes the behaviour of a Terraform resource with no backend, and `auto` approve.")
It("should be reconciled to have available outputs.")

const (
var (
sourceName = "test-tf-controller-no-output"
terraformName = "helloworld-no-outputs"
terraformName = "helloworld-no-outputs-" + rand.String(6)
)
g := NewWithT(t)
ctx := context.Background()
Expand Down Expand Up @@ -213,7 +214,7 @@ spec:
return err.Error()
}
return tfStateSecret.Name
}, timeout, interval).Should(Equal("secrets \"tfstate-custom-helloworld-no-outputs\" not found"))
}, timeout, interval).Should(Equal(fmt.Sprintf("secrets \"tfstate-custom-%s\" not found", terraformName)))
} else {
// TODO there's must be the default tfstate secret
}
Expand Down
7 changes: 4 additions & 3 deletions controllers/tc000012_src_bucket_no_outputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
)

// +kubebuilder:docs-gen:collapse=Imports
Expand All @@ -23,9 +24,9 @@ func Test_000012_src_bucket_no_outputs_test(t *testing.T) {
Spec("This spec describes the behaviour of a Terraform resource which is stored in an S3-compatible bucket. There is no backend and auto-approve is enabled.")
It("should be reconciled to have available outputs.")

const (
var (
sourceName = "test-tf-controller-src-bucket-no-output"
terraformName = "src-bucket-helloworld-no-outputs"
terraformName = "src-bucket-helloworld-no-outputs-" + rand.String(6)
)
g := NewWithT(t)
ctx := context.Background()
Expand Down Expand Up @@ -207,7 +208,7 @@ func Test_000012_src_bucket_no_outputs_test(t *testing.T) {
return err.Error()
}
return tfStateSecret.Name
}, timeout, interval).Should(Equal("secrets \"tfstate-default-src-bucket-helloworld-no-outputs\" not found"))
}, timeout, interval).Should(Equal(fmt.Sprintf("secrets \"tfstate-default-%s\" not found", terraformName)))
} else {
// TODO there's must be the default tfstate secret
}
Expand Down
53 changes: 53 additions & 0 deletions controllers/tc000016_default_observed_generation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package controllers

import (
"context"
"testing"
"time"

. "github.com/onsi/gomega"

infrav1 "github.com/weaveworks/tf-controller/api/v1alpha2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func Test_000016_default_observed_generation(t *testing.T) {
Spec("This spec describes default value of a Terraform resource")
It("should set the observedGeneration to -1 when the resource is created")

var (
terraformName = "tf-" + rand.String(6)
)
g := NewWithT(t)
ctx := context.Background()

Given("a Terraform resource")
By("creating a new TF resource.")
helloWorldTF := infrav1.Terraform{
ObjectMeta: metav1.ObjectMeta{
Name: terraformName,
Namespace: "default",
},
Spec: infrav1.TerraformSpec{
Path: "./terraform-hello-world-example",
SourceRef: infrav1.CrossNamespaceSourceReference{
Kind: "GitRepository",
Name: "foo",
Namespace: "flux-system",
},
Interval: metav1.Duration{Duration: time.Second * 10},
},
}
It("should be created and attached successfully.")
g.Expect(k8sClient.Create(ctx, &helloWorldTF)).Should(Succeed())
t.Cleanup(func() { g.Expect(k8sClient.Delete(ctx, &helloWorldTF)).Should(Succeed()) })

It("should have observedGeneration set to -1")
helloWorldTFKey := client.ObjectKeyFromObject(&helloWorldTF)
var createdHelloWorldTF infrav1.Terraform
g.Expect(k8sClient.Get(ctx, helloWorldTFKey, &createdHelloWorldTF)).To(Succeed())

g.Expect(createdHelloWorldTF.Status.ObservedGeneration).Should(Equal(int64(-1)))
}
113 changes: 113 additions & 0 deletions controllers/tc000016_finilize_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package controllers

import (
"context"
"testing"
"time"

"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
. "github.com/onsi/gomega"

infrav1 "github.com/weaveworks/tf-controller/api/v1alpha2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/rand"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func Test_000016_finalize_status(t *testing.T) {
Spec("This spec describes the behaviour of the Terraform controller when finalizing the status of a Terraform resource")
It("should set the observedGeneration and LastHandledReconcileAt after reconcile")

var (
sourceName = "test-finalize-status"
terraformName = "tf-" + rand.String(6)
)
g := NewWithT(t)
ctx := context.Background()

Given("a GitRepository")
By("defining a new GitRepository resource.")
updatedTime := time.Now()
testRepo := sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: sourceName,
Namespace: "flux-system",
},
Spec: sourcev1.GitRepositorySpec{
URL: "https://github.com/openshift-fluxv2-poc/podinfo",
Reference: &sourcev1.GitRepositoryRef{
Branch: "main",
},
Interval: metav1.Duration{Duration: time.Second * 30},
},
}

By("creating the GitRepository resource in the cluster.")
It("should be created successfully.")
g.Expect(k8sClient.Create(ctx, &testRepo)).Should(Succeed())
defer func() { g.Expect(k8sClient.Delete(ctx, &testRepo)).Should(Succeed()) }()

Given("the GitRepository's reconciled status")
By("setting the GitRepository's status, with the downloadable BLOB's URL, and the correct checksum.")
testRepo.Status = sourcev1.GitRepositoryStatus{
ObservedGeneration: int64(1),
Conditions: []metav1.Condition{
{
Type: "Ready",
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.Time{Time: updatedTime},
Reason: "GitOperationSucceed",
Message: "Fetched revision: main/b8e362c206e3d0cbb7ed22ced771a0056455a2fb",
},
},
Artifact: &sourcev1.Artifact{
Path: "gitrepository/flux-system/test-tf-controller/b8e362c206e3d0cbb7ed22ced771a0056455a2fb.tar.gz",
URL: server.URL() + "/file.tar.gz",
Revision: "main/b8e362c206e3d0cbb7ed22ced771a0056455a2fb",
Digest: "sha256:80ddfd18eb96f7d31cadc1a8a5171c6e2d95df3f6c23b0ed9cd8dddf6dba1406",
LastUpdateTime: metav1.Time{Time: updatedTime},
},
}

It("should be updated successfully.")
g.Expect(k8sClient.Status().Update(ctx, &testRepo)).Should(Succeed())

Given("a Terraform resource")
By("creating a new TF resource.")
helloWorldTF := infrav1.Terraform{
ObjectMeta: metav1.ObjectMeta{
Name: terraformName,
Namespace: "default",
Annotations: map[string]string{
meta.ReconcileRequestAnnotation: updatedTime.String(),
},
},
Spec: infrav1.TerraformSpec{
Path: "./terraform-hello-world-example",
SourceRef: infrav1.CrossNamespaceSourceReference{
Kind: "GitRepository",
Name: sourceName,
Namespace: "flux-system",
},
Interval: metav1.Duration{Duration: time.Second * 10},
},
}
It("should be created and attached successfully.")
g.Expect(k8sClient.Create(ctx, &helloWorldTF)).Should(Succeed())
t.Cleanup(func() { g.Expect(k8sClient.Delete(ctx, &helloWorldTF)).Should(Succeed()) })

It("should have observedGeneration and LastHandledReconcileAt set")
helloWorldTFKey := client.ObjectKeyFromObject(&helloWorldTF)
g.Eventually(func() int64 {
var createdHelloWorldTF infrav1.Terraform
g.Expect(k8sClient.Get(ctx, helloWorldTFKey, &createdHelloWorldTF)).To(Succeed())
return createdHelloWorldTF.Status.ObservedGeneration
}, timeout, interval).Should(Equal(int64(1)))

g.Eventually(func() string {
var createdHelloWorldTF infrav1.Terraform
g.Expect(k8sClient.Get(ctx, helloWorldTFKey, &createdHelloWorldTF)).To(Succeed())
return createdHelloWorldTF.Status.LastHandledReconcileAt
}, timeout, interval).Should(Equal(updatedTime.String()))
}
2 changes: 2 additions & 0 deletions controllers/tc000020_with_backend_no_outputs_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build flaky

package controllers

import (
Expand Down
2 changes: 2 additions & 0 deletions controllers/tc000030_plan_only_no_outputs_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build flaky

package controllers

import (
Expand Down
5 changes: 3 additions & 2 deletions controllers/tc000220_support_config_file_via_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
)

// +kubebuilder:docs-gen:collapse=Imports
Expand All @@ -25,9 +26,9 @@ func Test_000220_support_config_file_via_secret_test(t *testing.T) {
Spec("This spec describes the behaviour of a Terraform resource that has a config file attached.")
It("should generate the .tfrc file, and point the environment to that file so that the terraform binary could pick the correct configuration.")

const (
var (
sourceName = "tfrc-gitrepo-no-output"
terraformName = "tfrc-helloworld-no-outputs"
terraformName = "tfrc-helloworld-no-outputs" + rand.String(6)
)

const (
Expand Down
32 changes: 31 additions & 1 deletion controllers/tf_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/acl"
"github.com/fluxcd/pkg/runtime/conditions"
runtimeCtrl "github.com/fluxcd/pkg/runtime/controller"
"github.com/fluxcd/pkg/runtime/dependency"
"github.com/fluxcd/pkg/runtime/logger"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/pkg/runtime/predicates"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
Expand All @@ -46,6 +48,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/wait"
kuberecorder "k8s.io/client-go/tools/record"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
Expand Down Expand Up @@ -97,7 +100,7 @@ type TerraformReconciler struct {
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *TerraformReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
func (r *TerraformReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
reconcileStart := time.Now()
reconciliationLoopID := uuid.New().String()
log := ctrl.LoggerFrom(ctx, "reconciliation-loop-id", reconciliationLoopID, "start-time", reconcileStart)
Expand Down Expand Up @@ -126,7 +129,14 @@ func (r *TerraformReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}
log.Info(fmt.Sprintf(">> Started Generation: %d", terraform.GetGeneration()))

// Initialize the runtime patcher with the current version of the object.
patcher := patch.NewSerialPatcher(&terraform, r.Client)

defer func() {
if err := r.finalizeStatus(ctx, &terraform, patcher); err != nil {
retErr = kerrors.NewAggregate([]error{retErr, err})
}

// Record Prometheus metrics.
r.Metrics.RecordReadiness(ctx, &terraform)
r.Metrics.RecordSuspend(ctx, &terraform, terraform.Spec.Suspend)
Expand Down Expand Up @@ -807,3 +817,23 @@ func (r *TerraformReconciler) event(ctx context.Context, terraform infrav1.Terra
traceLog.Info("Add new annotated event")
r.EventRecorder.AnnotatedEventf(&terraform, metadata, eventType, reason, msg)
}

func (r *TerraformReconciler) finalizeStatus(ctx context.Context, obj *infrav1.Terraform, patcher *patch.SerialPatcher) error {
if v, ok := meta.ReconcileAnnotationValue(obj.GetAnnotations()); ok {
obj.Status.LastHandledReconcileAt = v
}

if conditions.IsTrue(obj, meta.ReadyCondition) {
obj.Status.ObservedGeneration = obj.Generation
}

if err := patcher.Patch(ctx, obj); err != nil {
if !obj.GetDeletionTimestamp().IsZero() {
err = kerrors.FilterOut(err, func(e error) bool { return apierrors.IsNotFound(e) })
}

return err
}

return nil
}

0 comments on commit 167ed67

Please sign in to comment.