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

Promote HelmRelease API to v2 (GA) #963

Merged
merged 10 commits into from
May 6, 2024
8 changes: 8 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ jobs:
kubectl -n helm-system rollout status deploy/helm-controller --timeout=1m
env:
KUBEBUILDER_ASSETS: ${{ github.workspace }}/kubebuilder/bin
- name: Test samples
run: |
kubectl create ns samples
kubectl -n samples apply -f config/samples
kubectl -n samples wait hr/podinfo-ocirepository --for=condition=ready --timeout=4m
kubectl -n samples wait hr/podinfo-gitrepository --for=condition=ready --timeout=4m
kubectl -n samples wait hr/podinfo-helmrepository --for=condition=ready --timeout=4m
kubectl delete ns samples
- name: Install sources
run: |
kubectl -n helm-system apply -f config/testdata/sources
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ manifests: controller-gen

# Generate API reference documentation
api-docs: gen-crd-api-reference-docs
$(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v2beta2 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v2beta2/helm.md
$(GEN_CRD_API_REFERENCE_DOCS) -api-dir=./api/v2 -config=./hack/api-docs/config.json -template-dir=./hack/api-docs/template -out-file=./docs/api/v2/helm.md

# Run go mod tidy
tidy:
Expand Down
5 changes: 4 additions & 1 deletion PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ resources:
- group: helm
kind: HelmRelease
version: v2beta2
storageVersion: v2beta2
- group: helm
kind: HelmRelease
version: v2
storageVersion: v2
version: "2"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ operator.
* Supports `HelmChart` artifacts produced from `HelmRepository`,
`GitRepository` and `Bucket` sources
* Fetches artifacts produced by [source-controller][] from `HelmChart`
objects
and `OCIRepository` objects
* Watches `HelmChart` objects for revision changes (including semver
ranges for charts from `HelmRepository` sources)
* Performs automated Helm actions, including Helm tests, rollbacks and
Expand All @@ -49,7 +49,7 @@ operator.

## Specifications

* [API](docs/spec/v2beta2/README.md)
* [API](docs/spec/v2/README.md)
* [Controller](docs/spec/README.md)

[source-controller]: https://github.com/fluxcd/source-controller
Expand Down
84 changes: 84 additions & 0 deletions api/v2/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
Copyright 2024 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v2

import "github.com/fluxcd/pkg/apis/meta"

const (
// ForceRequestAnnotation is the annotation used for triggering a one-off forced
// Helm release, even when there are no new changes in the HelmRelease.
// The value is interpreted as a token, and must equal the value of
// meta.ReconcileRequestAnnotation in order to trigger a release.
ForceRequestAnnotation string = "reconcile.fluxcd.io/forceAt"

// ResetRequestAnnotation is the annotation used for resetting the failure counts
// of a HelmRelease, so that it can be retried again.
// The value is interpreted as a token, and must equal the value of
// meta.ReconcileRequestAnnotation in order to reset the failure counts.
ResetRequestAnnotation string = "reconcile.fluxcd.io/resetAt"
)

// ShouldHandleResetRequest returns true if the HelmRelease has a reset request
// annotation, and the value of the annotation matches the value of the
// meta.ReconcileRequestAnnotation annotation.
//
// To ensure that the reset request is handled only once, the value of
// HelmReleaseStatus.LastHandledResetAt is updated to match the value of the
// reset request annotation (even if the reset request is not handled because
// the value of the meta.ReconcileRequestAnnotation annotation does not match).
func ShouldHandleResetRequest(obj *HelmRelease) bool {
return handleRequest(obj, ResetRequestAnnotation, &obj.Status.LastHandledResetAt)
}

// ShouldHandleForceRequest returns true if the HelmRelease has a force request
// annotation, and the value of the annotation matches the value of the
// meta.ReconcileRequestAnnotation annotation.
//
// To ensure that the force request is handled only once, the value of
// HelmReleaseStatus.LastHandledForceAt is updated to match the value of the
// force request annotation (even if the force request is not handled because
// the value of the meta.ReconcileRequestAnnotation annotation does not match).
func ShouldHandleForceRequest(obj *HelmRelease) bool {
return handleRequest(obj, ForceRequestAnnotation, &obj.Status.LastHandledForceAt)
}

// handleRequest returns true if the HelmRelease has a request annotation, and
// the value of the annotation matches the value of the meta.ReconcileRequestAnnotation
// annotation.
//
// The lastHandled argument is used to ensure that the request is handled only
// once, and is updated to match the value of the request annotation (even if
// the request is not handled because the value of the meta.ReconcileRequestAnnotation
// annotation does not match).
func handleRequest(obj *HelmRelease, annotation string, lastHandled *string) bool {
requestAt, requestOk := obj.GetAnnotations()[annotation]
reconcileAt, reconcileOk := meta.ReconcileAnnotationValue(obj.GetAnnotations())

var lastHandledRequest string
if requestOk {
lastHandledRequest = *lastHandled
*lastHandled = requestAt
}

if requestOk && reconcileOk && requestAt == reconcileAt {
lastHandledReconcile := obj.Status.GetLastHandledReconcileRequest()
if lastHandledReconcile != reconcileAt && lastHandledRequest != requestAt {
return true
}
}
return false
}
165 changes: 165 additions & 0 deletions api/v2/annotations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
Copyright 2024 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v2

import (
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/fluxcd/pkg/apis/meta"
)

func TestShouldHandleResetRequest(t *testing.T) {
t.Run("should handle reset request", func(t *testing.T) {
obj := &HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
meta.ReconcileRequestAnnotation: "b",
ResetRequestAnnotation: "b",
},
},
Status: HelmReleaseStatus{
LastHandledResetAt: "a",
ReconcileRequestStatus: meta.ReconcileRequestStatus{
LastHandledReconcileAt: "a",
},
},
}

if !ShouldHandleResetRequest(obj) {
t.Error("ShouldHandleResetRequest() = false")
}

if obj.Status.LastHandledResetAt != "b" {
t.Error("ShouldHandleResetRequest did not update LastHandledResetAt")
}
})
}

func TestShouldHandleForceRequest(t *testing.T) {
t.Run("should handle force request", func(t *testing.T) {
obj := &HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
meta.ReconcileRequestAnnotation: "b",
ForceRequestAnnotation: "b",
},
},
Status: HelmReleaseStatus{
LastHandledForceAt: "a",
ReconcileRequestStatus: meta.ReconcileRequestStatus{
LastHandledReconcileAt: "a",
},
},
}

if !ShouldHandleForceRequest(obj) {
t.Error("ShouldHandleForceRequest() = false")
}

if obj.Status.LastHandledForceAt != "b" {
t.Error("ShouldHandleForceRequest did not update LastHandledForceAt")
}
})
}

func Test_handleRequest(t *testing.T) {
const requestAnnotation = "requestAnnotation"

tests := []struct {
name string
annotations map[string]string
lastHandledReconcile string
lastHandledRequest string
want bool
expectLastHandledRequest string
}{
{
name: "valid request and reconcile annotations",
annotations: map[string]string{
meta.ReconcileRequestAnnotation: "b",
requestAnnotation: "b",
},
want: true,
expectLastHandledRequest: "b",
},
{
name: "mismatched annotations",
annotations: map[string]string{
meta.ReconcileRequestAnnotation: "b",
requestAnnotation: "c",
},
want: false,
expectLastHandledRequest: "c",
},
{
name: "reconcile matches previous request",
annotations: map[string]string{
meta.ReconcileRequestAnnotation: "b",
requestAnnotation: "b",
},
lastHandledReconcile: "a",
lastHandledRequest: "b",
want: false,
expectLastHandledRequest: "b",
},
{
name: "request matches previous reconcile",
annotations: map[string]string{
meta.ReconcileRequestAnnotation: "b",
requestAnnotation: "b",
},
lastHandledReconcile: "b",
lastHandledRequest: "a",
want: false,
expectLastHandledRequest: "b",
},
{
name: "missing annotations",
annotations: map[string]string{},
lastHandledRequest: "a",
want: false,
expectLastHandledRequest: "a",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
obj := &HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Annotations: tt.annotations,
},
Status: HelmReleaseStatus{
ReconcileRequestStatus: meta.ReconcileRequestStatus{
LastHandledReconcileAt: tt.lastHandledReconcile,
},
},
}

lastHandled := tt.lastHandledRequest
result := handleRequest(obj, requestAnnotation, &lastHandled)

if result != tt.want {
t.Errorf("handleRequest() = %v, want %v", result, tt.want)
}
if lastHandled != tt.expectLastHandledRequest {
t.Errorf("lastHandledRequest = %v, want %v", lastHandled, tt.expectLastHandledRequest)
}
})
}
}
82 changes: 82 additions & 0 deletions api/v2/condition_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright 2024 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v2

const (
// ReleasedCondition represents the status of the last release attempt
// (install/upgrade/test) against the latest desired state.
ReleasedCondition string = "Released"

// TestSuccessCondition represents the status of the last test attempt against
// the latest desired state.
TestSuccessCondition string = "TestSuccess"

// RemediatedCondition represents the status of the last remediation attempt
// (uninstall/rollback) due to a failure of the last release attempt against the
// latest desired state.
RemediatedCondition string = "Remediated"
)

const (
// InstallSucceededReason represents the fact that the Helm install for the
// HelmRelease succeeded.
InstallSucceededReason string = "InstallSucceeded"

// InstallFailedReason represents the fact that the Helm install for the
// HelmRelease failed.
InstallFailedReason string = "InstallFailed"

// UpgradeSucceededReason represents the fact that the Helm upgrade for the
// HelmRelease succeeded.
UpgradeSucceededReason string = "UpgradeSucceeded"

// UpgradeFailedReason represents the fact that the Helm upgrade for the
// HelmRelease failed.
UpgradeFailedReason string = "UpgradeFailed"

// TestSucceededReason represents the fact that the Helm tests for the
// HelmRelease succeeded.
TestSucceededReason string = "TestSucceeded"

// TestFailedReason represents the fact that the Helm tests for the HelmRelease
// failed.
TestFailedReason string = "TestFailed"

// RollbackSucceededReason represents the fact that the Helm rollback for the
// HelmRelease succeeded.
RollbackSucceededReason string = "RollbackSucceeded"

// RollbackFailedReason represents the fact that the Helm test for the
// HelmRelease failed.
RollbackFailedReason string = "RollbackFailed"

// UninstallSucceededReason represents the fact that the Helm uninstall for the
// HelmRelease succeeded.
UninstallSucceededReason string = "UninstallSucceeded"

// UninstallFailedReason represents the fact that the Helm uninstall for the
// HelmRelease failed.
UninstallFailedReason string = "UninstallFailed"

// ArtifactFailedReason represents the fact that the artifact download for the
// HelmRelease failed.
ArtifactFailedReason string = "ArtifactFailed"

// DependencyNotReadyReason represents the fact that
// one of the dependencies is not ready.
DependencyNotReadyReason string = "DependencyNotReady"
)
Loading