Skip to content

Rebase on top of upstream #28

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

Closed
wants to merge 10 commits into from
2 changes: 2 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
upstream-triage:
- "./*"
area/main-binary:
- 'main.go'
- 'internal/**/*'
Expand Down
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
name: CI

on:
push:
branches:
- '**'
pull_request:
branches: [ main ]
- push

jobs:

Expand Down
17 changes: 9 additions & 8 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
name: Deploy

on:
push:
branches:
- '**'
tags:
- 'v*'
pull_request:
branches: [ main ]
# Disabled as we don't need docker images to use the helm-operator as a library.
#on:
# push:
# branches:
# - '**'
# tags:
# - 'v*'
# pull_request:
# branches: [ main ]

jobs:
goreleaser:
Expand Down
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# helm-operator

![Build Status](https://github.com/operator-framework/helm-operator-plugins/workflows/CI/badge.svg?branch=master)
[![Coverage Status](https://coveralls.io/repos/github/operator-framework/helm-operator-plugins/badge.svg?branch=master)](https://coveralls.io/github/operator-framework/helm-operator-plugins?branch=master)
![Build Status](https://github.com/stackrox/helm-operator/workflows/CI/badge.svg?branch=main)

Reimplementation of the helm operator to enrich the Helm operator's reconciliation with custom Go code to create a
hybrid operator.
Expand Down Expand Up @@ -41,3 +40,45 @@ if err := reconciler.SetupWithManager(mgr); err != nil {
panic(fmt.Sprintf("unable to create reconciler: %s", err))
}
```

## Why a fork?

The Helm operator type automates Helm chart operations
by mapping the [values](https://helm.sh/docs/chart_template_guide/values_files/) of a Helm chart exactly to a
`CustomResourceDefinition` and defining its watched resources in a `watches.yaml`
[configuration](https://sdk.operatorframework.io/docs/building-operators/helm/tutorial/#watch-the-nginx-cr) file.

For creating a [Level II+](https://sdk.operatorframework.io/docs/advanced-topics/operator-capabilities/operator-capabilities/) operator
that reuses an already existing Helm chart, we need a [hybrid](https://github.com/operator-framework/operator-sdk/issues/670)
between the Go and Helm operator types.

The hybrid approach allows adding customizations to the Helm operator, such as:
- value mapping based on cluster state, or
- executing code in specific events.

### Quickstart

- Add this module as a replace directive to your `go.mod`:

```
go mod edit -replace=github.com/joelanford/helm-operator=github.com/stackrox/helm-operator@main
```

For example:

```go
chart, err := loader.Load("path/to/chart")
if err != nil {
panic(err)
}

reconciler := reconciler.New(
reconciler.WithChart(*chart),
reconciler.WithGroupVersionKind(gvk),
)

if err := reconciler.SetupWithManager(mgr); err != nil {
panic(fmt.Sprintf("unable to create reconciler: %s", err))
}
```

9 changes: 9 additions & 0 deletions pkg/client/actionclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type ActionInterface interface {
Get(name string, opts ...GetOption) (*release.Release, error)
Install(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...InstallOption) (*release.Release, error)
Upgrade(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...UpgradeOption) (*release.Release, error)
MarkFailed(release *release.Release, reason string) error
Uninstall(name string, opts ...UninstallOption) (*release.UninstallReleaseResponse, error)
Reconcile(rel *release.Release) error
}
Expand Down Expand Up @@ -180,6 +181,14 @@ func (c *actionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals m
return rel, nil
}

func (c *actionClient) MarkFailed(rel *release.Release, reason string) error {
infoCopy := *rel.Info
releaseCopy := *rel
releaseCopy.Info = &infoCopy
releaseCopy.SetStatus(release.StatusFailed, reason)
return c.conf.Releases.Update(&releaseCopy)
}

func (c *actionClient) Uninstall(name string, opts ...UninstallOption) (*release.UninstallReleaseResponse, error) {
uninstall := action.NewUninstall(c.conf)
for _, o := range opts {
Expand Down
17 changes: 17 additions & 0 deletions pkg/extensions/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package extensions

import (
"context"

"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// UpdateStatusFunc is a function that updates an unstructured status. If the status has been modified,
// true must be returned, false otherwise.
type UpdateStatusFunc func(*unstructured.Unstructured) bool

// ReconcileExtension is an arbitrary extension that can be implemented to run either before
// or after the main Helm reconciliation action.
// An error returned by a ReconcileExtension will cause the Reconcile to fail, unlike a hook error.
type ReconcileExtension func(context.Context, *unstructured.Unstructured, func(UpdateStatusFunc), logr.Logger) error
1 change: 1 addition & 0 deletions pkg/reconciler/internal/conditions/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
ReasonUpgradeError = status.ConditionReason("UpgradeError")
ReasonReconcileError = status.ConditionReason("ReconcileError")
ReasonUninstallError = status.ConditionReason("UninstallError")
ReasonPendingError = status.ConditionReason("PendingError")
)

func Initialized(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {
Expand Down
34 changes: 23 additions & 11 deletions pkg/reconciler/internal/fake/actionclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,19 @@ func (hcg *fakeActionClientGetter) ActionClientFor(_ crclient.Object) (client.Ac
}

type ActionClient struct {
Gets []GetCall
Installs []InstallCall
Upgrades []UpgradeCall
Uninstalls []UninstallCall
Reconciles []ReconcileCall

HandleGet func() (*release.Release, error)
HandleInstall func() (*release.Release, error)
HandleUpgrade func() (*release.Release, error)
HandleUninstall func() (*release.UninstallReleaseResponse, error)
HandleReconcile func() error
Gets []GetCall
Installs []InstallCall
Upgrades []UpgradeCall
MarkFaileds []MarkFailedCall
Uninstalls []UninstallCall
Reconciles []ReconcileCall

HandleGet func() (*release.Release, error)
HandleInstall func() (*release.Release, error)
HandleUpgrade func() (*release.Release, error)
HandleMarkFailed func() error
HandleUninstall func() (*release.UninstallReleaseResponse, error)
HandleReconcile func() error
}

func NewActionClient() ActionClient {
Expand Down Expand Up @@ -109,6 +111,11 @@ type UpgradeCall struct {
Opts []client.UpgradeOption
}

type MarkFailedCall struct {
Release *release.Release
Reason string
}

type UninstallCall struct {
Name string
Opts []client.UninstallOption
Expand All @@ -133,6 +140,11 @@ func (c *ActionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals m
return c.HandleUpgrade()
}

func (c *ActionClient) MarkFailed(rel *release.Release, reason string) error {
c.MarkFaileds = append(c.MarkFaileds, MarkFailedCall{rel, reason})
return c.HandleMarkFailed()
}

func (c *ActionClient) Uninstall(name string, opts ...client.UninstallOption) (*release.UninstallReleaseResponse, error) {
c.Uninstalls = append(c.Uninstalls, UninstallCall{name, opts})
return c.HandleUninstall()
Expand Down
39 changes: 34 additions & 5 deletions pkg/reconciler/internal/updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/operator-framework/helm-operator-plugins/internal/sdk/controllerutil"
"github.com/operator-framework/helm-operator-plugins/pkg/extensions"
"github.com/operator-framework/helm-operator-plugins/pkg/internal/status"
)

Expand All @@ -53,6 +54,21 @@ func (u *Updater) UpdateStatus(fs ...UpdateStatusFunc) {
u.updateStatusFuncs = append(u.updateStatusFuncs, fs...)
}

func (u *Updater) UpdateStatusCustom(f extensions.UpdateStatusFunc) {
updateFn := func(status *helmAppStatus) bool {
status.updateStatusObject()

unstructuredStatus := unstructured.Unstructured{Object: status.StatusObject}
if !f(&unstructuredStatus) {
return false
}
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredStatus.Object, status)
status.StatusObject = unstructuredStatus.Object
return true
}
u.UpdateStatus(updateFn)
}

func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) error {
backoff := retry.DefaultRetry

Expand All @@ -66,11 +82,8 @@ func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) err
needsStatusUpdate = f(st) || needsStatusUpdate
}
if needsStatusUpdate {
uSt, err := runtime.DefaultUnstructuredConverter.ToUnstructured(st)
if err != nil {
return err
}
obj.Object["status"] = uSt
st.updateStatusObject()
obj.Object["status"] = st.StatusObject
return u.client.Status().Update(ctx, obj)
}
return nil
Expand Down Expand Up @@ -149,10 +162,25 @@ func RemoveDeployedRelease() UpdateStatusFunc {
}

type helmAppStatus struct {
StatusObject map[string]interface{} `json:"-"`

Conditions status.Conditions `json:"conditions"`
DeployedRelease *helmAppRelease `json:"deployedRelease,omitempty"`
}

func (s *helmAppStatus) updateStatusObject() {
unstructuredHelmAppStatus, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(s)
if s.StatusObject == nil {
s.StatusObject = make(map[string]interface{})
}
s.StatusObject["conditions"] = unstructuredHelmAppStatus["conditions"]
if deployedRelease := unstructuredHelmAppStatus["deployedRelease"]; deployedRelease != nil {
s.StatusObject["deployedRelease"] = deployedRelease
} else {
delete(s.StatusObject, "deployedRelease")
}
}

type helmAppRelease struct {
Name string `json:"name,omitempty"`
Manifest string `json:"manifest,omitempty"`
Expand All @@ -175,6 +203,7 @@ func statusFor(obj *unstructured.Unstructured) *helmAppStatus {
case map[string]interface{}:
out := &helmAppStatus{}
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(s, out)
out.StatusObject = s
return out
default:
return &helmAppStatus{}
Expand Down
76 changes: 73 additions & 3 deletions pkg/reconciler/internal/updater/updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import (
"github.com/operator-framework/helm-operator-plugins/pkg/reconciler/internal/conditions"
)

const testFinalizer = "testFinalizer"
const (
testFinalizer = "testFinalizer"
availableReplicasStatus = int64(3)
replicasStatus = int64(5)
)

var _ = Describe("Updater", func() {
var (
Expand Down Expand Up @@ -86,6 +90,71 @@ var _ = Describe("Updater", func() {
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(1))
Expect(obj.GetResourceVersion()).NotTo(Equal(resourceVersion))
})

It("should support a mix of standard and custom status updates", func() {
u.UpdateStatus(EnsureCondition(conditions.Deployed(corev1.ConditionTrue, "", "")))
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
Expect(unstructured.SetNestedField(uSt.Object, replicasStatus, "replicas")).To(Succeed())
return true
})
u.UpdateStatus(EnsureCondition(conditions.Irreconcilable(corev1.ConditionFalse, "", "")))
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
Expect(unstructured.SetNestedField(uSt.Object, availableReplicasStatus, "availableReplicas")).To(Succeed())
return true
})
u.UpdateStatus(EnsureCondition(conditions.Initialized(corev1.ConditionTrue, "", "")))

Expect(u.Apply(context.TODO(), obj)).To(Succeed())
Expect(client.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(3))
_, found, err := unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
Expect(found).To(BeFalse())
Expect(err).To(Not(HaveOccurred()))

val, found, err := unstructured.NestedInt64(obj.Object, "status", "replicas")
Expect(val).To(Equal(replicasStatus))
Expect(found).To(BeTrue())
Expect(err).To(Not(HaveOccurred()))

val, found, err = unstructured.NestedInt64(obj.Object, "status", "availableReplicas")
Expect(val).To(Equal(availableReplicasStatus))
Expect(found).To(BeTrue())
Expect(err).To(Not(HaveOccurred()))
})

It("should preserve any custom status across multiple apply calls", func() {
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
Expect(unstructured.SetNestedField(uSt.Object, int64(5), "replicas")).To(Succeed())
return true
})
Expect(u.Apply(context.TODO(), obj)).To(Succeed())

Expect(client.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())

_, found, err := unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
Expect(found).To(BeFalse())
Expect(err).To(Not(HaveOccurred()))

val, found, err := unstructured.NestedInt64(obj.Object, "status", "replicas")
Expect(val).To(Equal(replicasStatus))
Expect(found).To(BeTrue())
Expect(err).To(Succeed())

u.UpdateStatus(EnsureCondition(conditions.Deployed(corev1.ConditionTrue, "", "")))
Expect(u.Apply(context.TODO(), obj)).To(Succeed())

Expect(client.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(1))

_, found, err = unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
Expect(found).To(BeFalse())
Expect(err).To(Not(HaveOccurred()))

val, found, err = unstructured.NestedInt64(obj.Object, "status", "replicas")
Expect(val).To(Equal(replicasStatus))
Expect(found).To(BeTrue())
Expect(err).To(Succeed())
})
})
})

Expand Down Expand Up @@ -241,8 +310,9 @@ var _ = Describe("statusFor", func() {
})

It("should handle map[string]interface{}", func() {
obj.Object["status"] = map[string]interface{}{}
Expect(statusFor(obj)).To(Equal(&helmAppStatus{}))
uSt := map[string]interface{}{}
obj.Object["status"] = uSt
Expect(statusFor(obj)).To(Equal(&helmAppStatus{StatusObject: uSt}))
})

It("should handle arbitrary types", func() {
Expand Down
Loading