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

Rebase 20230804 1 #44

Closed
wants to merge 13 commits into from
Closed
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
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=main)
[![Coverage Status](https://coveralls.io/repos/github/operator-framework/helm-operator-plugins/badge.svg?branch=main)](https://coveralls.io/github/operator-framework/helm-operator-plugins?branch=main)
![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 @@ -53,6 +53,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 @@ -264,6 +265,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) rollback(name string, opts ...RollbackOption) error {
rollback := action.NewRollback(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
13 changes: 10 additions & 3 deletions pkg/reconciler/internal/conditions/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ const (
TypeDeployed = "Deployed"
TypeReleaseFailed = "ReleaseFailed"
TypeIrreconcilable = "Irreconcilable"
TypePaused = "Paused"

ReasonInstallSuccessful = status.ConditionReason("InstallSuccessful")
ReasonUpgradeSuccessful = status.ConditionReason("UpgradeSuccessful")
ReasonUninstallSuccessful = status.ConditionReason("UninstallSuccessful")
ReasonInstallSuccessful = status.ConditionReason("InstallSuccessful")
ReasonUpgradeSuccessful = status.ConditionReason("UpgradeSuccessful")
ReasonUninstallSuccessful = status.ConditionReason("UninstallSuccessful")
ReasonPauseReconcileAnnotationTrue = status.ConditionReason("PauseReconcileAnnotationTrue")

ReasonErrorGettingClient = status.ConditionReason("ErrorGettingClient")
ReasonErrorGettingValues = status.ConditionReason("ErrorGettingValues")
Expand All @@ -41,6 +43,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 All @@ -59,6 +62,10 @@ func Irreconcilable(stat corev1.ConditionStatus, reason status.ConditionReason,
return newCondition(TypeIrreconcilable, stat, reason, message)
}

func Paused(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {
return newCondition(TypePaused, stat, reason, message)
}

func newCondition(t status.ConditionType, s corev1.ConditionStatus, r status.ConditionReason, m interface{}) status.Condition {
message := fmt.Sprintf("%s", m)
return 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
45 changes: 40 additions & 5 deletions pkg/reconciler/internal/updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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 Down Expand Up @@ -56,6 +57,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) CancelUpdates() {
u.isCanceled = true
}
Expand Down Expand Up @@ -94,11 +110,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 @@ -147,6 +160,12 @@ func EnsureConditionUnknown(t status.ConditionType) UpdateStatusFunc {
}
}

func EnsureConditionAbsent(t status.ConditionType) UpdateStatusFunc {
return func(status *helmAppStatus) bool {
return status.Conditions.RemoveCondition(t)
}
}

func EnsureDeployedRelease(rel *release.Release) UpdateStatusFunc {
return func(status *helmAppStatus) bool {
newRel := helmAppReleaseFor(rel)
Expand All @@ -167,10 +186,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 @@ -193,6 +227,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
Loading
Loading