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

Allow setting up dependent watches for some resource types, even if dependent watches are disabled #19

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
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
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
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
# helm-operator

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

Experimental refactoring of the operator-framework's helm operator.

## 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.

## Quick start

- 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))
}
```

Experimental refactoring of the operator-framework's helm operator
2 changes: 1 addition & 1 deletion internal/cmd/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func (r *run) run(cmd *cobra.Command) {
os.Exit(1)
}

if err := r.SetupWithManager(mgr); err != nil {
if err := r.SetupWithManager(mgr, reconciler.SetupOpts{}); err != nil {
log.Error(err, "unable to create controller", "controller", "Helm")
os.Exit(1)
}
Expand Down
39 changes: 33 additions & 6 deletions pkg/annotation/annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

// Package annotation allows to set custom install, upgrade or uninstall options on custom resource objects with annotations.
// To create custom annotations implement the Install, Upgrade or Uninstall interface.
//
// Example:
//
// To disable hooks based on annotations the InstallDisableHooks is passed to the reconciler as an option.
//
// r, err := reconciler.New(
// reconciler.WithChart(*w.Chart),
// reconciler.WithGroupVersionKind(w.GroupVersionKind),
// reconciler.WithInstallAnnotations(annotation.InstallDisableHooks{}),
// )
//
// If the reconciler detects an annotation named "helm.sdk.operatorframework.io/install-disable-hooks"
// on the watched custom resource, it sets the install.DisableHooks option to the annotations value. For more information
// take a look at the InstallDisableHooks.InstallOption method.
//
// kind: OperatorHelmKind
// apiVersion: test.example.com/v1
// metadata:
// name: nginx-sample
// annotations:
// "helm.sdk.operatorframework.io/install-disable-hooks": true
//
package annotation

import (
Expand All @@ -30,27 +54,24 @@ var (
DefaultUninstallAnnotations = []Uninstall{UninstallDescription{}, UninstallDisableHooks{}}
)

// Install configures an install annotation.
type Install interface {
Name() string
InstallOption(string) helmclient.InstallOption
}

// Upgrade configures an upgrade annotation.
type Upgrade interface {
Name() string
UpgradeOption(string) helmclient.UpgradeOption
}

// Uninstall configures an uninstall annotation.
type Uninstall interface {
Name() string
UninstallOption(string) helmclient.UninstallOption
}

type InstallDisableHooks struct {
CustomName string
}

var _ Install = &InstallDisableHooks{}

const (
defaultDomain = "helm.sdk.operatorframework.io"
defaultInstallDisableHooksName = defaultDomain + "/install-disable-hooks"
Expand All @@ -64,6 +85,12 @@ const (
defaultUninstallDescriptionName = defaultDomain + "/uninstall-description"
)

type InstallDisableHooks struct {
CustomName string
}

var _ Install = &InstallDisableHooks{}

func (i InstallDisableHooks) Name() string {
if i.CustomName != "" {
return i.CustomName
Expand Down
10 changes: 10 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 @@ -163,6 +164,7 @@ func (c *actionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals m
if rel != nil {
rollback := action.NewRollback(c.conf)
rollback.Force = true
rollback.MaxHistory = upgrade.MaxHistory

// As of Helm 2.13, if Upgrade returns a non-nil release, that
// means the release was also recorded in the release store.
Expand All @@ -179,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
49 changes: 34 additions & 15 deletions pkg/reconciler/internal/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,22 @@ import (
"github.com/joelanford/helm-operator/pkg/manifestutil"
)

func NewDependentResourceWatcher(c controller.Controller, rm meta.RESTMapper) hook.PostHook {
func NewDependentResourceWatcher(c controller.Controller, rm meta.RESTMapper, watchReleaseResources bool, extraWatches ...schema.GroupVersionKind) hook.PostHook {
Comment on lines 39 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is confusing to have watchReleaseResources and extraWatches as parameters in one function and letting the caller decide which one to choose.

Imho adding two functions with different names delegating to a private newDependentResourceWatcher is preferred by me.

// NewDependentResourceWatcherForResources adds depended watches on specific configured resources. 
func NewDependentResourceWatcherForResources(c controller.Controller, rm meta.RESTMapper, watches ...schema.GroupVersionKind) hook.PostHook {
  return newDependentResourceWatcher(c, rm, false, watches)
}

// NewDependentReleaseResourceWatcher adds watchers to all objects created by the Helm release.
func NewDependentReleaseResourceWatcher(c controller.Controller, rm meta.RESTMapper) hook.PostHook {
  newDependentResourceWatcher(c, rm, true)
}

Writing documentation for it is imho more complex and involves mental overhead to understand the concept to why I should add extraWatches if I already added all watched dependencies by setting watchReleaseResources = true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But with this change we can no longer separate the "skip dependent watches" and "watch extra resources" parameters. This has to be a single watcher, we cannot just call this function once with watchReleaseResources = true and then with the extra watches; if we ever go back to not skipping dependent watches, this would cause two reconciliations for each update to the resources specified here. Maybe not a concern because we would just drop the extra watches then, but it does complicate the option design substantially.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree!

return &dependentResourceWatcher{
controller: c,
restMapper: rm,
m: sync.Mutex{},
watches: make(map[schema.GroupVersionKind]struct{}),
controller: c,
restMapper: rm,
watchReleaseResources: watchReleaseResources,
extraWatches: extraWatches,
m: sync.Mutex{},
watches: make(map[schema.GroupVersionKind]struct{}),
}
}

type dependentResourceWatcher struct {
controller controller.Controller
restMapper meta.RESTMapper
controller controller.Controller
restMapper meta.RESTMapper
watchReleaseResources bool
extraWatches []schema.GroupVersionKind

m sync.Mutex
watches map[schema.GroupVersionKind]struct{}
Expand All @@ -58,16 +62,31 @@ func (d *dependentResourceWatcher) Exec(owner *unstructured.Unstructured, rel re
// using predefined functions for filtering events
dependentPredicate := predicate.DependentPredicateFuncs()

resources := releaseutil.SplitManifests(rel.Manifest)
d.m.Lock()
defer d.m.Unlock()
for _, r := range resources {
var obj unstructured.Unstructured
err := yaml.Unmarshal([]byte(r), &obj)
if err != nil {
return err
var allWatches []unstructured.Unstructured
if d.watchReleaseResources {
resources := releaseutil.SplitManifests(rel.Manifest)
for _, r := range resources {
var obj unstructured.Unstructured
err := yaml.Unmarshal([]byte(r), &obj)
if err != nil {
return err
}

allWatches = append(allWatches, obj)
}
}

// For extra watches, we only support same namespace
for _, extraWatchGVK := range d.extraWatches {
var obj unstructured.Unstructured
obj.SetGroupVersionKind(extraWatchGVK)
obj.SetNamespace(owner.GetNamespace())
allWatches = append(allWatches, obj)
}

d.m.Lock()
defer d.m.Unlock()
for _, obj := range allWatches {
depGVK := obj.GroupVersionKind()
if _, ok := d.watches[depGVK]; ok || depGVK.Empty() {
continue
Expand Down
Loading