pin dependencies

Signed-off-by: Roland.Ma <rolandma@yunify.com>
This commit is contained in:
Roland.Ma
2021-08-16 03:29:03 +00:00
parent eae248b3c9
commit 7bb8124a61
251 changed files with 40010 additions and 716 deletions

View File

@@ -0,0 +1,239 @@
/*
Copyright 2020 The Operator-SDK 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 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.InstallDisableHook{}),
// )
//
// 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 (
"strconv"
"helm.sh/helm/v3/pkg/action"
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
)
var (
DefaultInstallAnnotations = []Install{InstallDescription{}, InstallDisableHooks{}}
DefaultUpgradeAnnotations = []Upgrade{UpgradeDescription{}, UpgradeDisableHooks{}, UpgradeForce{}}
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 install annotation.
type Uninstall interface {
Name() string
UninstallOption(string) helmclient.UninstallOption
}
const (
defaultDomain = "helm.sdk.operatorframework.io"
defaultInstallDisableHooksName = defaultDomain + "/install-disable-hooks"
defaultUpgradeDisableHooksName = defaultDomain + "/upgrade-disable-hooks"
defaultUninstallDisableHooksName = defaultDomain + "/uninstall-disable-hooks"
defaultUpgradeForceName = defaultDomain + "/upgrade-force"
defaultInstallDescriptionName = defaultDomain + "/install-description"
defaultUpgradeDescriptionName = defaultDomain + "/upgrade-description"
defaultUninstallDescriptionName = defaultDomain + "/uninstall-description"
)
type InstallDisableHooks struct {
CustomName string
}
var _ Install = &InstallDisableHooks{}
func (i InstallDisableHooks) Name() string {
if i.CustomName != "" {
return i.CustomName
}
return defaultInstallDisableHooksName
}
func (i InstallDisableHooks) InstallOption(val string) helmclient.InstallOption {
disableHooks := false
if v, err := strconv.ParseBool(val); err == nil {
disableHooks = v
}
return func(install *action.Install) error {
install.DisableHooks = disableHooks
return nil
}
}
type UpgradeDisableHooks struct {
CustomName string
}
var _ Upgrade = &UpgradeDisableHooks{}
func (u UpgradeDisableHooks) Name() string {
if u.CustomName != "" {
return u.CustomName
}
return defaultUpgradeDisableHooksName
}
func (u UpgradeDisableHooks) UpgradeOption(val string) helmclient.UpgradeOption {
disableHooks := false
if v, err := strconv.ParseBool(val); err == nil {
disableHooks = v
}
return func(upgrade *action.Upgrade) error {
upgrade.DisableHooks = disableHooks
return nil
}
}
type UpgradeForce struct {
CustomName string
}
var _ Upgrade = &UpgradeForce{}
func (u UpgradeForce) Name() string {
if u.CustomName != "" {
return u.CustomName
}
return defaultUpgradeForceName
}
func (u UpgradeForce) UpgradeOption(val string) helmclient.UpgradeOption {
force := false
if v, err := strconv.ParseBool(val); err == nil {
force = v
}
return func(upgrade *action.Upgrade) error {
upgrade.Force = force
return nil
}
}
type UninstallDisableHooks struct {
CustomName string
}
var _ Uninstall = &UninstallDisableHooks{}
func (u UninstallDisableHooks) Name() string {
if u.CustomName != "" {
return u.CustomName
}
return defaultUninstallDisableHooksName
}
func (u UninstallDisableHooks) UninstallOption(val string) helmclient.UninstallOption {
disableHooks := false
if v, err := strconv.ParseBool(val); err == nil {
disableHooks = v
}
return func(uninstall *action.Uninstall) error {
uninstall.DisableHooks = disableHooks
return nil
}
}
var _ Install = &InstallDescription{}
type InstallDescription struct {
CustomName string
}
func (i InstallDescription) Name() string {
if i.CustomName != "" {
return i.CustomName
}
return defaultInstallDescriptionName
}
func (i InstallDescription) InstallOption(v string) helmclient.InstallOption {
return func(i *action.Install) error {
i.Description = v
return nil
}
}
var _ Upgrade = &UpgradeDescription{}
type UpgradeDescription struct {
CustomName string
}
func (u UpgradeDescription) Name() string {
if u.CustomName != "" {
return u.CustomName
}
return defaultUpgradeDescriptionName
}
func (u UpgradeDescription) UpgradeOption(v string) helmclient.UpgradeOption {
return func(upgrade *action.Upgrade) error {
upgrade.Description = v
return nil
}
}
var _ Uninstall = &UninstallDescription{}
type UninstallDescription struct {
CustomName string
}
func (u UninstallDescription) Name() string {
if u.CustomName != "" {
return u.CustomName
}
return defaultUninstallDescriptionName
}
func (u UninstallDescription) UninstallOption(v string) helmclient.UninstallOption {
return func(uninstall *action.Uninstall) error {
uninstall.Description = v
return nil
}
}

View File

@@ -0,0 +1,351 @@
/*
Copyright 2020 The Operator-SDK 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 client
import (
"bytes"
"encoding/json"
"errors"
"fmt"
sdkhandler "github.com/operator-framework/operator-lib/handler"
"gomodules.xyz/jsonpatch/v2"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/kube"
helmkube "helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
apitypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/operator-framework/helm-operator-plugins/pkg/internal/sdk/controllerutil"
"github.com/operator-framework/helm-operator-plugins/pkg/manifestutil"
)
type ActionClientGetter interface {
ActionClientFor(obj client.Object) (ActionInterface, error)
}
type ActionClientGetterFunc func(obj client.Object) (ActionInterface, error)
func (acgf ActionClientGetterFunc) ActionClientFor(obj client.Object) (ActionInterface, error) {
return acgf(obj)
}
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)
Uninstall(name string, opts ...UninstallOption) (*release.UninstallReleaseResponse, error)
Reconcile(rel *release.Release) error
}
type GetOption func(*action.Get) error
type InstallOption func(*action.Install) error
type UpgradeOption func(*action.Upgrade) error
type UninstallOption func(*action.Uninstall) error
func NewActionClientGetter(acg ActionConfigGetter) ActionClientGetter {
return &actionClientGetter{acg}
}
type actionClientGetter struct {
acg ActionConfigGetter
}
var _ ActionClientGetter = &actionClientGetter{}
func (hcg *actionClientGetter) ActionClientFor(obj client.Object) (ActionInterface, error) {
actionConfig, err := hcg.acg.ActionConfigFor(obj)
if err != nil {
return nil, err
}
rm, err := actionConfig.RESTClientGetter.ToRESTMapper()
if err != nil {
return nil, err
}
postRenderer := createPostRenderer(rm, actionConfig.KubeClient, obj)
return &actionClient{actionConfig, postRenderer}, nil
}
type actionClient struct {
conf *action.Configuration
postRenderer postrender.PostRenderer
}
var _ ActionInterface = &actionClient{}
func (c *actionClient) Get(name string, opts ...GetOption) (*release.Release, error) {
get := action.NewGet(c.conf)
for _, o := range opts {
if err := o(get); err != nil {
return nil, err
}
}
return get.Run(name)
}
func (c *actionClient) Install(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...InstallOption) (*release.Release, error) {
install := action.NewInstall(c.conf)
install.PostRenderer = c.postRenderer
for _, o := range opts {
if err := o(install); err != nil {
return nil, err
}
}
install.ReleaseName = name
install.Namespace = namespace
c.conf.Log("Starting install")
rel, err := install.Run(chrt, vals)
if err != nil {
c.conf.Log("Install failed")
if rel != nil {
// Uninstall the failed release installation so that we can retry
// the installation again during the next reconciliation. In many
// cases, the issue is unresolvable without a change to the CR, but
// controller-runtime will backoff on retries after failed attempts.
//
// In certain cases, Install will return a partial release in
// the response even when it doesn't record the release in its release
// store (e.g. when there is an error rendering the release manifest).
// In that case the rollback will fail with a not found error because
// there was nothing to rollback.
//
// Only return an error about a rollback failure if the failure was
// caused by something other than the release not being found.
_, uninstallErr := c.Uninstall(name)
if !errors.Is(uninstallErr, driver.ErrReleaseNotFound) {
return nil, fmt.Errorf("uninstall failed: %v: original install error: %w", uninstallErr, err)
}
}
return nil, err
}
return rel, nil
}
func (c *actionClient) Upgrade(name, namespace string, chrt *chart.Chart, vals map[string]interface{}, opts ...UpgradeOption) (*release.Release, error) {
upgrade := action.NewUpgrade(c.conf)
upgrade.PostRenderer = c.postRenderer
for _, o := range opts {
if err := o(upgrade); err != nil {
return nil, err
}
}
upgrade.Namespace = namespace
rel, err := upgrade.Run(name, chrt, vals)
if err != nil {
if rel != nil {
rollback := action.NewRollback(c.conf)
rollback.Force = true
// As of Helm 2.13, if Upgrade returns a non-nil release, that
// means the release was also recorded in the release store.
// Therefore, we should perform the rollback when we have a non-nil
// release. Any rollback error here would be unexpected, so always
// log both the update and rollback errors.
rollbackErr := rollback.Run(name)
if rollbackErr != nil {
return nil, fmt.Errorf("rollback failed: %v: original upgrade error: %w", rollbackErr, err)
}
}
return nil, err
}
return rel, nil
}
func (c *actionClient) Uninstall(name string, opts ...UninstallOption) (*release.UninstallReleaseResponse, error) {
uninstall := action.NewUninstall(c.conf)
for _, o := range opts {
if err := o(uninstall); err != nil {
return nil, err
}
}
return uninstall.Run(name)
}
func (c *actionClient) Reconcile(rel *release.Release) error {
infos, err := c.conf.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false)
if err != nil {
return err
}
return infos.Visit(func(expected *resource.Info, err error) error {
if err != nil {
return fmt.Errorf("visit error: %w", err)
}
helper := resource.NewHelper(expected.Client, expected.Mapping)
existing, err := helper.Get(expected.Namespace, expected.Name)
if apierrors.IsNotFound(err) {
if _, err := helper.Create(expected.Namespace, true, expected.Object); err != nil {
return fmt.Errorf("create error: %w", err)
}
return nil
} else if err != nil {
return fmt.Errorf("could not get object: %w", err)
}
patch, patchType, err := createPatch(existing, expected)
if err != nil {
return fmt.Errorf("error creating patch: %w", err)
}
if patch == nil {
// nothing to do
return nil
}
_, err = helper.Patch(expected.Namespace, expected.Name, patchType, patch,
&metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("patch error: %w", err)
}
return nil
})
}
func createPatch(existing runtime.Object, expected *resource.Info) ([]byte, apitypes.PatchType, error) {
existingJSON, err := json.Marshal(existing)
if err != nil {
return nil, apitypes.StrategicMergePatchType, err
}
expectedJSON, err := json.Marshal(expected.Object)
if err != nil {
return nil, apitypes.StrategicMergePatchType, err
}
// Get a versioned object
versionedObject := helmkube.AsVersioned(expected)
// Unstructured objects, such as CRDs, may not have an not registered error
// returned from ConvertToVersion. Anything that's unstructured should
// use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
// on objects like CRDs.
_, isUnstructured := versionedObject.(runtime.Unstructured)
// On newer K8s versions, CRDs aren't unstructured but has this dedicated type
_, isCRDv1beta1 := versionedObject.(*apiextv1beta1.CustomResourceDefinition)
_, isCRDv1 := versionedObject.(*apiextv1.CustomResourceDefinition)
if isUnstructured || isCRDv1beta1 || isCRDv1 {
// fall back to generic JSON merge patch
patch, err := createJSONMergePatch(existingJSON, expectedJSON)
return patch, apitypes.JSONPatchType, err
}
patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
if err != nil {
return nil, apitypes.StrategicMergePatchType, err
}
patch, err := strategicpatch.CreateThreeWayMergePatch(expectedJSON, expectedJSON, existingJSON, patchMeta, true)
return patch, apitypes.StrategicMergePatchType, err
}
func createJSONMergePatch(existingJSON, expectedJSON []byte) ([]byte, error) {
ops, err := jsonpatch.CreatePatch(existingJSON, expectedJSON)
if err != nil {
return nil, err
}
// We ignore the "remove" operations from the full patch because they are
// fields added by Kubernetes or by the user after the existing release
// resource has been applied. The goal for this patch is to make sure that
// the fields managed by the Helm chart are applied.
// All "add" operations without a value (null) can be ignored
patchOps := make([]jsonpatch.JsonPatchOperation, 0)
for _, op := range ops {
if op.Operation != "remove" && !(op.Operation == "add" && op.Value == nil) {
patchOps = append(patchOps, op)
}
}
// If there are no patch operations, return nil. Callers are expected
// to check for a nil response and skip the patch operation to avoid
// unnecessary chatter with the API server.
if len(patchOps) == 0 {
return nil, nil
}
return json.Marshal(patchOps)
}
func createPostRenderer(rm meta.RESTMapper, kubeClient kube.Interface, owner client.Object) postrender.PostRenderer {
return &ownerPostRenderer{rm, kubeClient, owner}
}
type ownerPostRenderer struct {
rm meta.RESTMapper
kubeClient kube.Interface
owner client.Object
}
func (pr *ownerPostRenderer) Run(in *bytes.Buffer) (*bytes.Buffer, error) {
resourceList, err := pr.kubeClient.Build(in, false)
if err != nil {
return nil, err
}
out := bytes.Buffer{}
err = resourceList.Visit(func(r *resource.Info, err error) error {
if err != nil {
return err
}
objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r.Object)
if err != nil {
return err
}
u := &unstructured.Unstructured{Object: objMap}
useOwnerRef, err := controllerutil.SupportsOwnerReference(pr.rm, pr.owner, u)
if err != nil {
return err
}
if useOwnerRef && !manifestutil.HasResourcePolicyKeep(u.GetAnnotations()) {
ownerRef := metav1.NewControllerRef(pr.owner, pr.owner.GetObjectKind().GroupVersionKind())
ownerRefs := append(u.GetOwnerReferences(), *ownerRef)
u.SetOwnerReferences(ownerRefs)
} else {
if err := sdkhandler.SetOwnerAnnotations(pr.owner, u); err != nil {
return err
}
}
outData, err := yaml.Marshal(u.Object)
if err != nil {
return err
}
if _, err := out.WriteString("---\n" + string(outData)); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return &out, nil
}

View File

@@ -0,0 +1,117 @@
/*
Copyright 2020 The Operator-SDK 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 client
import (
"context"
"fmt"
"github.com/go-logr/logr"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"sigs.k8s.io/controller-runtime/pkg/client"
)
type ActionConfigGetter interface {
ActionConfigFor(obj client.Object) (*action.Configuration, error)
}
func NewActionConfigGetter(cfg *rest.Config, rm meta.RESTMapper, log logr.Logger) ActionConfigGetter {
return &actionConfigGetter{
cfg: cfg,
restMapper: rm,
log: log,
}
}
var _ ActionConfigGetter = &actionConfigGetter{}
type actionConfigGetter struct {
cfg *rest.Config
restMapper meta.RESTMapper
log logr.Logger
}
func (acg *actionConfigGetter) ActionConfigFor(obj client.Object) (*action.Configuration, error) {
// Create a RESTClientGetter
rcg := newRESTClientGetter(acg.cfg, acg.restMapper, obj.GetNamespace())
// Setup the debug log function that Helm will use
debugLog := func(format string, v ...interface{}) {
if acg.log != nil {
acg.log.V(1).Info(fmt.Sprintf(format, v...))
}
}
// Create a client that helm will use to manage release resources.
// The passed object is used as an owner reference on every
// object the client creates.
kc := kube.New(rcg)
kc.Log = debugLog
// Create the Kubernetes Secrets client. The passed object is
// also used as an owner reference in the release secrets
// created by this client.
kcs, err := cmdutil.NewFactory(rcg).KubernetesClientSet()
if err != nil {
return nil, err
}
ownerRef := metav1.NewControllerRef(obj, obj.GetObjectKind().GroupVersionKind())
d := driver.NewSecrets(&ownerRefSecretClient{
SecretInterface: kcs.CoreV1().Secrets(obj.GetNamespace()),
refs: []metav1.OwnerReference{*ownerRef},
})
// Also, use the debug log for the storage driver
d.Log = debugLog
// Initialize the storage backend
s := storage.Init(d)
return &action.Configuration{
RESTClientGetter: rcg,
Releases: s,
KubeClient: kc,
Log: debugLog,
}, nil
}
var _ v1.SecretInterface = &ownerRefSecretClient{}
type ownerRefSecretClient struct {
v1.SecretInterface
refs []metav1.OwnerReference
}
func (c *ownerRefSecretClient) Create(ctx context.Context, in *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
in.OwnerReferences = append(in.OwnerReferences, c.refs...)
return c.SecretInterface.Create(ctx, in, opts)
}
func (c *ownerRefSecretClient) Update(ctx context.Context, in *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
in.OwnerReferences = append(in.OwnerReferences, c.refs...)
return c.SecretInterface.Update(ctx, in, opts)
}

View File

@@ -0,0 +1,100 @@
/*
Copyright 2020 The Operator-SDK 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 client
import (
"sync"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/discovery"
cached "k8s.io/client-go/discovery/cached"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
var _ genericclioptions.RESTClientGetter = &restClientGetter{}
func newRESTClientGetter(cfg *rest.Config, rm meta.RESTMapper, ns string) genericclioptions.RESTClientGetter {
return &restClientGetter{
restConfig: cfg,
restMapper: rm,
namespaceConfig: &namespaceClientConfig{ns},
}
}
type restClientGetter struct {
restConfig *rest.Config
restMapper meta.RESTMapper
namespaceConfig clientcmd.ClientConfig
setupDiscoveryClient sync.Once
cachedDiscoveryClient discovery.CachedDiscoveryInterface
}
func (c *restClientGetter) ToRESTConfig() (*rest.Config, error) {
return c.restConfig, nil
}
func (c *restClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
var (
dc discovery.DiscoveryInterface
err error
)
c.setupDiscoveryClient.Do(func() {
dc, err = discovery.NewDiscoveryClientForConfig(c.restConfig)
if err != nil {
return
}
c.cachedDiscoveryClient = cached.NewMemCacheClient(dc)
})
if err != nil {
return nil, err
}
return c.cachedDiscoveryClient, nil
}
func (c *restClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
return c.restMapper, nil
}
func (c *restClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return c.namespaceConfig
}
var _ clientcmd.ClientConfig = &namespaceClientConfig{}
type namespaceClientConfig struct {
namespace string
}
func (c namespaceClientConfig) RawConfig() (clientcmdapi.Config, error) {
return clientcmdapi.Config{}, nil
}
func (c namespaceClientConfig) ClientConfig() (*rest.Config, error) {
return nil, nil
}
func (c namespaceClientConfig) Namespace() (string, bool, error) {
return c.namespace, false, nil
}
func (c namespaceClientConfig) ConfigAccess() clientcmd.ConfigAccess {
return nil
}

View File

@@ -0,0 +1,44 @@
/*
Copyright 2020 The Operator-SDK 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 hook
import (
"github.com/go-logr/logr"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type PreHook interface {
Exec(*unstructured.Unstructured, chartutil.Values, logr.Logger) error
}
type PreHookFunc func(*unstructured.Unstructured, chartutil.Values, logr.Logger) error
func (f PreHookFunc) Exec(obj *unstructured.Unstructured, vals chartutil.Values, log logr.Logger) error {
return f(obj, vals, log)
}
type PostHook interface {
Exec(*unstructured.Unstructured, release.Release, logr.Logger) error
}
type PostHookFunc func(*unstructured.Unstructured, release.Release, logr.Logger) error
func (f PostHookFunc) Exec(obj *unstructured.Unstructured, rel release.Release, log logr.Logger) error {
return f(obj, rel, log)
}

View File

@@ -0,0 +1,90 @@
/*
Copyright 2020 The Operator-SDK 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 controllerutil
import (
"context"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
var (
AddFinalizer = controllerutil.AddFinalizer
RemoveFinalizer = controllerutil.RemoveFinalizer
ContainsFinalizer = func(obj metav1.Object, finalizer string) bool {
for _, f := range obj.GetFinalizers() {
if f == finalizer {
return true
}
}
return false
}
)
func WaitForDeletion(ctx context.Context, cl client.Reader, o client.Object) error {
key := client.ObjectKeyFromObject(o)
return wait.PollImmediateUntil(time.Millisecond*10, func() (bool, error) {
err := cl.Get(ctx, key, o)
if apierrors.IsNotFound(err) {
return true, nil
}
if err != nil {
return false, err
}
return false, nil
}, ctx.Done())
}
func SupportsOwnerReference(restMapper meta.RESTMapper, owner, dependent client.Object) (bool, error) {
ownerGVK := owner.GetObjectKind().GroupVersionKind()
ownerMapping, err := restMapper.RESTMapping(ownerGVK.GroupKind(), ownerGVK.Version)
if err != nil {
return false, err
}
depGVK := dependent.GetObjectKind().GroupVersionKind()
depMapping, err := restMapper.RESTMapping(depGVK.GroupKind(), depGVK.Version)
if err != nil {
return false, err
}
ownerClusterScoped := ownerMapping.Scope.Name() == meta.RESTScopeNameRoot
ownerNamespace := owner.GetNamespace()
depClusterScoped := depMapping.Scope.Name() == meta.RESTScopeNameRoot
depNamespace := dependent.GetNamespace()
if ownerClusterScoped {
return true, nil
}
if depClusterScoped {
return false, nil
}
if ownerNamespace != depNamespace {
return false, nil
}
return true, nil
}

View File

@@ -0,0 +1,81 @@
/*
Copyright 2020 The Operator-SDK 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 predicate
import (
"reflect"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/event"
logf "sigs.k8s.io/controller-runtime/pkg/log"
crtpredicate "sigs.k8s.io/controller-runtime/pkg/predicate"
)
var log = logf.Log.WithName("predicate")
type GenerationChangedPredicate = crtpredicate.GenerationChangedPredicate
// DependentPredicateFuncs returns functions defined for filtering events
func DependentPredicateFuncs() crtpredicate.Funcs {
dependentPredicate := crtpredicate.Funcs{
// We don't need to reconcile dependent resource creation events
// because dependent resources are only ever created during
// reconciliation. Another reconcile would be redundant.
CreateFunc: func(e event.CreateEvent) bool {
o := e.Object.(*unstructured.Unstructured)
log.V(1).Info("Skipping reconciliation for dependent resource creation", "name", o.GetName(), "namespace", o.GetNamespace(), "apiVersion", o.GroupVersionKind().GroupVersion(), "kind", o.GroupVersionKind().Kind)
return false
},
// Reconcile when a dependent resource is deleted so that it can be
// recreated.
DeleteFunc: func(e event.DeleteEvent) bool {
o := e.Object.(*unstructured.Unstructured)
log.V(1).Info("Reconciling due to dependent resource deletion", "name", o.GetName(), "namespace", o.GetNamespace(), "apiVersion", o.GroupVersionKind().GroupVersion(), "kind", o.GroupVersionKind().Kind)
return true
},
// Don't reconcile when a generic event is received for a dependent
GenericFunc: func(e event.GenericEvent) bool {
o := e.Object.(*unstructured.Unstructured)
log.V(1).Info("Skipping reconcile due to generic event", "name", o.GetName(), "namespace", o.GetNamespace(), "apiVersion", o.GroupVersionKind().GroupVersion(), "kind", o.GroupVersionKind().Kind)
return false
},
// Reconcile when a dependent resource is updated, so that it can
// be patched back to the resource managed by the CR, if
// necessary. Ignore updates that only change the status and
// resourceVersion.
UpdateFunc: func(e event.UpdateEvent) bool {
old := e.ObjectOld.(*unstructured.Unstructured).DeepCopy()
new := e.ObjectNew.(*unstructured.Unstructured).DeepCopy()
delete(old.Object, "status")
delete(new.Object, "status")
old.SetResourceVersion("")
new.SetResourceVersion("")
if reflect.DeepEqual(old.Object, new.Object) {
return false
}
log.V(1).Info("Reconciling due to dependent resource update", "name", new.GetName(), "namespace", new.GetNamespace(), "apiVersion", new.GroupVersionKind().GroupVersion(), "kind", new.GroupVersionKind().Kind)
return true
},
}
return dependentPredicate
}

View File

@@ -0,0 +1,191 @@
/*
Copyright 2020 The Operator-SDK 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 status
import (
"encoding/json"
"sort"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kubeclock "k8s.io/apimachinery/pkg/util/clock"
)
// clock is used to set status condition timestamps.
// This variable makes it easier to test conditions.
var clock kubeclock.Clock = &kubeclock.RealClock{}
// ConditionType is the type of the condition and is typically a CamelCased
// word or short phrase.
//
// Condition types should indicate state in the "abnormal-true" polarity. For
// example, if the condition indicates when a policy is invalid, the "is valid"
// case is probably the norm, so the condition should be called "Invalid".
type ConditionType string
// ConditionReason is intended to be a one-word, CamelCase representation of
// the category of cause of the current status. It is intended to be used in
// concise output, such as one-line kubectl get output, and in summarizing
// occurrences of causes.
type ConditionReason string
// Condition represents an observation of an object's state. Conditions are an
// extension mechanism intended to be used when the details of an observation
// are not a priori known or would not apply to all instances of a given Kind.
//
// Conditions should be added to explicitly convey properties that users and
// components care about rather than requiring those properties to be inferred
// from other observations. Once defined, the meaning of a Condition can not be
// changed arbitrarily - it becomes part of the API, and has the same
// backwards- and forwards-compatibility concerns of any other part of the API.
type Condition struct {
Type ConditionType `json:"type"`
Status corev1.ConditionStatus `json:"status"`
Reason ConditionReason `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
}
// IsTrue Condition whether the condition status is "True".
func (c Condition) IsTrue() bool {
return c.Status == corev1.ConditionTrue
}
// IsFalse returns whether the condition status is "False".
func (c Condition) IsFalse() bool {
return c.Status == corev1.ConditionFalse
}
// IsUnknown returns whether the condition status is "Unknown".
func (c Condition) IsUnknown() bool {
return c.Status == corev1.ConditionUnknown
}
// DeepCopyInto copies in into out.
func (c *Condition) DeepCopyInto(cpy *Condition) {
*cpy = *c
}
// Conditions is a set of Condition instances.
type Conditions []Condition
// NewConditions initializes a set of conditions with the given list of
// conditions.
func NewConditions(conds ...Condition) Conditions {
conditions := Conditions{}
for _, c := range conds {
conditions.SetCondition(c)
}
return conditions
}
// IsTrueFor searches the set of conditions for a condition with the given
// ConditionType. If found, it returns `condition.IsTrue()`. If not found,
// it returns false.
func (conditions Conditions) IsTrueFor(t ConditionType) bool {
for _, condition := range conditions {
if condition.Type == t {
return condition.IsTrue()
}
}
return false
}
// IsFalseFor searches the set of conditions for a condition with the given
// ConditionType. If found, it returns `condition.IsFalse()`. If not found,
// it returns false.
func (conditions Conditions) IsFalseFor(t ConditionType) bool {
for _, condition := range conditions {
if condition.Type == t {
return condition.IsFalse()
}
}
return false
}
// IsUnknownFor searches the set of conditions for a condition with the given
// ConditionType. If found, it returns `condition.IsUnknown()`. If not found,
// it returns true.
func (conditions Conditions) IsUnknownFor(t ConditionType) bool {
for _, condition := range conditions {
if condition.Type == t {
return condition.IsUnknown()
}
}
return true
}
// SetCondition adds (or updates) the set of conditions with the given
// condition. It returns a boolean value indicating whether the set condition
// is new or was a change to the existing condition with the same type.
func (conditions *Conditions) SetCondition(newCond Condition) bool {
newCond.LastTransitionTime = metav1.Time{Time: clock.Now()}
for i, condition := range *conditions {
if condition.Type == newCond.Type {
if condition.Status == newCond.Status {
newCond.LastTransitionTime = condition.LastTransitionTime
}
changed := condition.Status != newCond.Status ||
condition.Reason != newCond.Reason ||
condition.Message != newCond.Message
(*conditions)[i] = newCond
return changed
}
}
*conditions = append(*conditions, newCond)
return true
}
// GetCondition searches the set of conditions for the condition with the given
// ConditionType and returns it. If the matching condition is not found,
// GetCondition returns nil.
func (conditions Conditions) GetCondition(t ConditionType) *Condition {
for _, condition := range conditions {
if condition.Type == t {
return &condition
}
}
return nil
}
// RemoveCondition removes the condition with the given ConditionType from
// the conditions set. If no condition with that type is found, RemoveCondition
// returns without performing any action. If the passed condition type is not
// found in the set of conditions, RemoveCondition returns false.
func (conditions *Conditions) RemoveCondition(t ConditionType) bool {
if conditions == nil {
return false
}
for i, condition := range *conditions {
if condition.Type == t {
*conditions = append((*conditions)[:i], (*conditions)[i+1:]...)
return true
}
}
return false
}
// MarshalJSON marshals the set of conditions as a JSON array, sorted by
// condition type.
func (conditions Conditions) MarshalJSON() ([]byte, error) {
conds := []Condition(conditions)
sort.Slice(conds, func(a, b int) bool {
return conds[a].Type < conds[b].Type
})
return json.Marshal(conds)
}

View File

@@ -0,0 +1,35 @@
/*
Copyright 2021 The Operator-SDK 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 manifestutil
import (
"strings"
"helm.sh/helm/v3/pkg/kube"
)
func HasResourcePolicyKeep(annotations map[string]string) bool {
if annotations == nil {
return false
}
resourcePolicyType, ok := annotations[kube.ResourcePolicyAnno]
if !ok {
return false
}
resourcePolicyType = strings.ToLower(strings.TrimSpace(resourcePolicyType))
return resourcePolicyType == kube.KeepPolicy
}

View File

@@ -0,0 +1,70 @@
/*
Copyright 2020 The Operator-SDK 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 conditions
import (
"fmt"
corev1 "k8s.io/api/core/v1"
"github.com/operator-framework/helm-operator-plugins/pkg/internal/sdk/status"
)
const (
TypeInitialized = "Initialized"
TypeDeployed = "Deployed"
TypeReleaseFailed = "ReleaseFailed"
TypeIrreconcilable = "Irreconcilable"
ReasonInstallSuccessful = status.ConditionReason("InstallSuccessful")
ReasonUpgradeSuccessful = status.ConditionReason("UpgradeSuccessful")
ReasonUninstallSuccessful = status.ConditionReason("UninstallSuccessful")
ReasonErrorGettingClient = status.ConditionReason("ErrorGettingClient")
ReasonErrorGettingValues = status.ConditionReason("ErrorGettingValues")
ReasonErrorGettingReleaseState = status.ConditionReason("ErrorGettingReleaseState")
ReasonInstallError = status.ConditionReason("InstallError")
ReasonUpgradeError = status.ConditionReason("UpgradeError")
ReasonReconcileError = status.ConditionReason("ReconcileError")
ReasonUninstallError = status.ConditionReason("UninstallError")
)
func Initialized(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {
return newCondition(TypeInitialized, stat, reason, message)
}
func Deployed(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {
return newCondition(TypeDeployed, stat, reason, message)
}
func ReleaseFailed(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {
return newCondition(TypeReleaseFailed, stat, reason, message)
}
func Irreconcilable(stat corev1.ConditionStatus, reason status.ConditionReason, message interface{}) status.Condition {
return newCondition(TypeIrreconcilable, 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{
Type: t,
Status: s,
Reason: r,
Message: message,
}
}

View File

@@ -0,0 +1,100 @@
/*
Copyright 2020 The Operator-SDK 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 hook
import (
"sync"
"github.com/go-logr/logr"
sdkhandler "github.com/operator-framework/operator-lib/handler"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/source"
"sigs.k8s.io/yaml"
"github.com/operator-framework/helm-operator-plugins/pkg/hook"
"github.com/operator-framework/helm-operator-plugins/pkg/internal/sdk/controllerutil"
"github.com/operator-framework/helm-operator-plugins/pkg/internal/sdk/predicate"
"github.com/operator-framework/helm-operator-plugins/pkg/manifestutil"
)
func NewDependentResourceWatcher(c controller.Controller, rm meta.RESTMapper) hook.PostHook {
return &dependentResourceWatcher{
controller: c,
restMapper: rm,
m: sync.Mutex{},
watches: make(map[schema.GroupVersionKind]struct{}),
}
}
type dependentResourceWatcher struct {
controller controller.Controller
restMapper meta.RESTMapper
m sync.Mutex
watches map[schema.GroupVersionKind]struct{}
}
func (d *dependentResourceWatcher) Exec(owner *unstructured.Unstructured, rel release.Release, log logr.Logger) error {
// 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
}
depGVK := obj.GroupVersionKind()
if _, ok := d.watches[depGVK]; ok || depGVK.Empty() {
continue
}
useOwnerRef, err := controllerutil.SupportsOwnerReference(d.restMapper, owner, &obj)
if err != nil {
return err
}
if useOwnerRef && !manifestutil.HasResourcePolicyKeep(obj.GetAnnotations()) {
if err := d.controller.Watch(&source.Kind{Type: &obj}, &handler.EnqueueRequestForOwner{
OwnerType: owner,
IsController: true,
}, dependentPredicate); err != nil {
return err
}
} else {
if err := d.controller.Watch(&source.Kind{Type: &obj}, &sdkhandler.EnqueueRequestForAnnotation{
Type: owner.GetObjectKind().GroupVersionKind().GroupKind(),
}, dependentPredicate); err != nil {
return err
}
}
d.watches[depGVK] = struct{}{}
log.V(1).Info("Watching dependent resource", "dependentAPIVersion", depGVK.GroupVersion(), "dependentKind", depGVK.Kind)
}
return nil
}

View File

@@ -0,0 +1,192 @@
/*
Copyright 2020 The Operator-SDK 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 updater
import (
"context"
"helm.sh/helm/v3/pkg/release"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/operator-framework/helm-operator-plugins/pkg/internal/sdk/controllerutil"
"github.com/operator-framework/helm-operator-plugins/pkg/internal/sdk/status"
)
func New(client client.Client) Updater {
return Updater{
client: client,
}
}
type Updater struct {
client client.Client
updateFuncs []UpdateFunc
updateStatusFuncs []UpdateStatusFunc
}
type UpdateFunc func(*unstructured.Unstructured) bool
type UpdateStatusFunc func(*helmAppStatus) bool
func (u *Updater) Update(fs ...UpdateFunc) {
u.updateFuncs = append(u.updateFuncs, fs...)
}
func (u *Updater) UpdateStatus(fs ...UpdateStatusFunc) {
u.updateStatusFuncs = append(u.updateStatusFuncs, fs...)
}
func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) error {
backoff := retry.DefaultRetry
// Always update the status first. During uninstall, if
// we remove the finalizer, updating the status will fail
// because the object and its status will be garbage-collected
if err := retry.RetryOnConflict(backoff, func() error {
st := statusFor(obj)
needsStatusUpdate := false
for _, f := range u.updateStatusFuncs {
needsStatusUpdate = f(st) || needsStatusUpdate
}
if needsStatusUpdate {
uSt, err := runtime.DefaultUnstructuredConverter.ToUnstructured(st)
if err != nil {
return err
}
obj.Object["status"] = uSt
return u.client.Status().Update(ctx, obj)
}
return nil
}); err != nil {
return err
}
if err := retry.RetryOnConflict(backoff, func() error {
needsUpdate := false
for _, f := range u.updateFuncs {
needsUpdate = f(obj) || needsUpdate
}
if needsUpdate {
return u.client.Update(ctx, obj)
}
return nil
}); err != nil {
return err
}
return nil
}
func EnsureFinalizer(finalizer string) UpdateFunc {
return func(obj *unstructured.Unstructured) bool {
if controllerutil.ContainsFinalizer(obj, finalizer) {
return false
}
controllerutil.AddFinalizer(obj, finalizer)
return true
}
}
func RemoveFinalizer(finalizer string) UpdateFunc {
return func(obj *unstructured.Unstructured) bool {
if !controllerutil.ContainsFinalizer(obj, finalizer) {
return false
}
controllerutil.RemoveFinalizer(obj, finalizer)
return true
}
}
func EnsureCondition(condition status.Condition) UpdateStatusFunc {
return func(status *helmAppStatus) bool {
return status.Conditions.SetCondition(condition)
}
}
func EnsureConditionUnknown(t status.ConditionType) UpdateStatusFunc {
return func(s *helmAppStatus) bool {
return s.Conditions.SetCondition(status.Condition{
Type: t,
Status: corev1.ConditionUnknown,
})
}
}
func EnsureDeployedRelease(rel *release.Release) UpdateStatusFunc {
return func(status *helmAppStatus) bool {
newRel := helmAppReleaseFor(rel)
if status.DeployedRelease == nil && newRel == nil {
return false
}
if status.DeployedRelease != nil && newRel != nil &&
*status.DeployedRelease == *newRel {
return false
}
status.DeployedRelease = newRel
return true
}
}
func RemoveDeployedRelease() UpdateStatusFunc {
return EnsureDeployedRelease(nil)
}
type helmAppStatus struct {
Conditions status.Conditions `json:"conditions"`
DeployedRelease *helmAppRelease `json:"deployedRelease,omitempty"`
}
type helmAppRelease struct {
Name string `json:"name,omitempty"`
Manifest string `json:"manifest,omitempty"`
}
func statusFor(obj *unstructured.Unstructured) *helmAppStatus {
if obj == nil || obj.Object == nil {
return nil
}
status, ok := obj.Object["status"]
if !ok {
return &helmAppStatus{}
}
switch s := status.(type) {
case *helmAppStatus:
return s
case helmAppStatus:
return &s
case map[string]interface{}:
out := &helmAppStatus{}
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(s, out)
return out
default:
return &helmAppStatus{}
}
}
func helmAppReleaseFor(rel *release.Release) *helmAppRelease {
if rel == nil {
return nil
}
return &helmAppRelease{
Name: rel.Name,
Manifest: rel.Manifest,
}
}

View File

@@ -0,0 +1,70 @@
/*
Copyright 2020 The Operator-SDK 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 values
import (
"fmt"
"os"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/strvals"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/operator-framework/helm-operator-plugins/pkg/values"
)
type Values struct {
m map[string]interface{}
}
func FromUnstructured(obj *unstructured.Unstructured) (*Values, error) {
if obj == nil || obj.Object == nil {
return nil, fmt.Errorf("nil object")
}
spec, ok := obj.Object["spec"]
if !ok {
return nil, fmt.Errorf("spec not found")
}
specMap, ok := spec.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("spec must be a map")
}
return New(specMap), nil
}
func New(m map[string]interface{}) *Values {
return &Values{m: m}
}
func (v *Values) Map() map[string]interface{} {
if v == nil {
return nil
}
return v.m
}
func (v *Values) ApplyOverrides(in map[string]string) error {
for inK, inV := range in {
val := fmt.Sprintf("%s=%s", inK, os.ExpandEnv(inV))
if err := strvals.ParseInto(val, v.m); err != nil {
return err
}
}
return nil
}
var DefaultMapper = values.MapperFunc(func(v chartutil.Values) chartutil.Values { return v })

View File

@@ -0,0 +1,797 @@
/*
Copyright 2020 The Operator-SDK 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 reconciler
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/go-logr/logr"
sdkhandler "github.com/operator-framework/operator-lib/handler"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/operator-framework/helm-operator-plugins/pkg/annotation"
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
"github.com/operator-framework/helm-operator-plugins/pkg/hook"
"github.com/operator-framework/helm-operator-plugins/pkg/internal/sdk/controllerutil"
"github.com/operator-framework/helm-operator-plugins/pkg/reconciler/internal/conditions"
internalhook "github.com/operator-framework/helm-operator-plugins/pkg/reconciler/internal/hook"
"github.com/operator-framework/helm-operator-plugins/pkg/reconciler/internal/updater"
internalvalues "github.com/operator-framework/helm-operator-plugins/pkg/reconciler/internal/values"
"github.com/operator-framework/helm-operator-plugins/pkg/values"
)
const uninstallFinalizer = "uninstall-helm-release"
// Reconciler reconciles a Helm object
type Reconciler struct {
client client.Client
actionClientGetter helmclient.ActionClientGetter
valueMapper values.Mapper
eventRecorder record.EventRecorder
preHooks []hook.PreHook
postHooks []hook.PostHook
log logr.Logger
gvk *schema.GroupVersionKind
chrt *chart.Chart
overrideValues map[string]string
skipDependentWatches bool
maxConcurrentReconciles int
reconcilePeriod time.Duration
annotSetupOnce sync.Once
annotations map[string]struct{}
installAnnotations map[string]annotation.Install
upgradeAnnotations map[string]annotation.Upgrade
uninstallAnnotations map[string]annotation.Uninstall
}
// New creates a new Reconciler that reconciles custom resources that define a
// Helm release. New takes variadic Option arguments that are used to configure
// the Reconciler.
//
// Required options are:
// - WithGroupVersionKind
// - WithChart
//
// Other options are defaulted to sane defaults when SetupWithManager is called.
//
// If an error occurs configuring or validating the Reconciler, it is returned.
func New(opts ...Option) (*Reconciler, error) {
r := &Reconciler{}
r.annotSetupOnce.Do(r.setupAnnotationMaps)
for _, o := range opts {
if err := o(r); err != nil {
return nil, err
}
}
if err := r.validate(); err != nil {
return nil, err
}
return r, nil
}
func (r *Reconciler) setupAnnotationMaps() {
r.annotations = make(map[string]struct{})
r.installAnnotations = make(map[string]annotation.Install)
r.upgradeAnnotations = make(map[string]annotation.Upgrade)
r.uninstallAnnotations = make(map[string]annotation.Uninstall)
}
// SetupWithManager configures a controller for the Reconciler and registers
// watches. It also uses the passed Manager to initialize default values for the
// Reconciler and sets up the manager's scheme with the Reconciler's configured
// GroupVersionKind.
//
// If an error occurs setting up the Reconciler with the manager, it is
// returned.
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
controllerName := fmt.Sprintf("%v-controller", strings.ToLower(r.gvk.Kind))
r.addDefaults(mgr, controllerName)
r.setupScheme(mgr)
c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: r, MaxConcurrentReconciles: r.maxConcurrentReconciles})
if err != nil {
return err
}
if err := r.setupWatches(mgr, c); err != nil {
return err
}
r.log.Info("Watching resource",
"group", r.gvk.Group,
"version", r.gvk.Version,
"kind", r.gvk.Kind,
)
return nil
}
// Option is a function that configures the helm Reconciler.
type Option func(r *Reconciler) error
// WithClient is an Option that configures a Reconciler's client.
//
// By default, manager.GetClient() is used if this option is not configured.
func WithClient(cl client.Client) Option {
return func(r *Reconciler) error {
r.client = cl
return nil
}
}
// WithActionClientGetter is an Option that configures a Reconciler's
// ActionClientGetter.
//
// A default ActionClientGetter is used if this option is not configured.
func WithActionClientGetter(actionClientGetter helmclient.ActionClientGetter) Option {
return func(r *Reconciler) error {
r.actionClientGetter = actionClientGetter
return nil
}
}
// WithEventRecorder is an Option that configures a Reconciler's EventRecorder.
//
// By default, manager.GetEventRecorderFor() is used if this option is not
// configured.
func WithEventRecorder(er record.EventRecorder) Option {
return func(r *Reconciler) error {
r.eventRecorder = er
return nil
}
}
// WithLog is an Option that configures a Reconciler's logger.
//
// A default logger is used if this option is not configured.
func WithLog(log logr.Logger) Option {
return func(r *Reconciler) error {
r.log = log
return nil
}
}
// WithGroupVersionKind is an Option that configures a Reconciler's
// GroupVersionKind.
//
// This option is required.
func WithGroupVersionKind(gvk schema.GroupVersionKind) Option {
return func(r *Reconciler) error {
r.gvk = &gvk
return nil
}
}
// WithChart is an Option that configures a Reconciler's helm chart.
//
// This option is required.
func WithChart(chrt chart.Chart) Option {
return func(r *Reconciler) error {
r.chrt = &chrt
return nil
}
}
// WithOverrideValues is an Option that configures a Reconciler's override
// values.
//
// Override values can be used to enforce that certain values provided by the
// chart's default values.yaml or by a CR spec are always overridden when
// rendering the chart. If a value in overrides is set by a CR, it is
// overridden by the override value. The override value can be static but can
// also refer to an environment variable.
//
// If an environment variable reference is listed in override values but is not
// present in the environment when this function runs, it will resolve to an
// empty string and override all other values. Therefore, when using
// environment variable expansion, ensure that the environment variable is set.
func WithOverrideValues(overrides map[string]string) Option {
return func(r *Reconciler) error {
// Validate that overrides can be parsed and applied
// so that we fail fast during operator setup rather
// than during the first reconciliation.
m := internalvalues.New(map[string]interface{}{})
if err := m.ApplyOverrides(overrides); err != nil {
return err
}
r.overrideValues = overrides
return nil
}
}
// WithDependentWatchesEnabled is an Option that configures whether the
// Reconciler will register watches for dependent objects in releases and
// trigger reconciliations when they change.
//
// By default, dependent watches are enabled.
func SkipDependentWatches(skip bool) Option {
return func(r *Reconciler) error {
r.skipDependentWatches = skip
return nil
}
}
// WithMaxConcurrentReconciles is an Option that configures the number of
// concurrent reconciles that the controller will run.
//
// The default is 1.
func WithMaxConcurrentReconciles(max int) Option {
return func(r *Reconciler) error {
if max < 1 {
return errors.New("maxConcurrentReconciles must be at least 1")
}
r.maxConcurrentReconciles = max
return nil
}
}
// WithReconcilePeriod is an Option that configures the reconcile period of the
// controller. This will cause the controller to reconcile CRs at least once
// every period. By default, the reconcile period is set to 0, which means no
// time-based reconciliations will occur.
func WithReconcilePeriod(rp time.Duration) Option {
return func(r *Reconciler) error {
if rp < 0 {
return errors.New("reconcile period must not be negative")
}
r.reconcilePeriod = rp
return nil
}
}
// WithInstallAnnotations is an Option that configures Install annotations
// to enable custom action.Install fields to be set based on the value of
// annotations found in the custom resource watched by this reconciler.
// Duplicate annotation names will result in an error.
func WithInstallAnnotations(as ...annotation.Install) Option {
return func(r *Reconciler) error {
r.annotSetupOnce.Do(r.setupAnnotationMaps)
for _, a := range as {
name := a.Name()
if _, ok := r.annotations[name]; ok {
return fmt.Errorf("annotation %q already exists", name)
}
r.annotations[name] = struct{}{}
r.installAnnotations[name] = a
}
return nil
}
}
// WithUpgradeAnnotations is an Option that configures Upgrade annotations
// to enable custom action.Upgrade fields to be set based on the value of
// annotations found in the custom resource watched by this reconciler.
// Duplicate annotation names will result in an error.
func WithUpgradeAnnotations(as ...annotation.Upgrade) Option {
return func(r *Reconciler) error {
r.annotSetupOnce.Do(r.setupAnnotationMaps)
for _, a := range as {
name := a.Name()
if _, ok := r.annotations[name]; ok {
return fmt.Errorf("annotation %q already exists", name)
}
r.annotations[name] = struct{}{}
r.upgradeAnnotations[name] = a
}
return nil
}
}
// WithUninstallAnnotations is an Option that configures Uninstall annotations
// to enable custom action.Uninstall fields to be set based on the value of
// annotations found in the custom resource watched by this reconciler.
// Duplicate annotation names will result in an error.
func WithUninstallAnnotations(as ...annotation.Uninstall) Option {
return func(r *Reconciler) error {
r.annotSetupOnce.Do(r.setupAnnotationMaps)
for _, a := range as {
name := a.Name()
if _, ok := r.annotations[name]; ok {
return fmt.Errorf("annotation %q already exists", name)
}
r.annotations[name] = struct{}{}
r.uninstallAnnotations[name] = a
}
return nil
}
}
// WithPreHook is an Option that configures the reconciler to run the given
// PreHook just before performing any actions (e.g. install, upgrade, uninstall,
// or reconciliation).
func WithPreHook(h hook.PreHook) Option {
return func(r *Reconciler) error {
r.preHooks = append(r.preHooks, h)
return nil
}
}
// WithPostHook is an Option that configures the reconciler to run the given
// PostHook just after performing any non-uninstall release actions.
func WithPostHook(h hook.PostHook) Option {
return func(r *Reconciler) error {
r.postHooks = append(r.postHooks, h)
return nil
}
}
// WithValueMapper is an Option that configures a function that maps values
// from a custom resource spec to the values passed to Helm
func WithValueMapper(m values.Mapper) Option {
return func(r *Reconciler) error {
r.valueMapper = m
return nil
}
}
// Reconcile reconciles a CR that defines a Helm v3 release.
//
// - If a release does not exist for this CR, a new release is installed.
// - If a release exists and the CR spec has changed since the last,
// reconciliation, the release is upgraded.
// - If a release exists and the CR spec has not changed since the last
// reconciliation, the release is reconciled. Any dependent resources that
// have diverged from the release manifest are re-created or patched so that
// they are re-aligned with the release.
// - If the CR has been deleted, the release will be uninstalled. The
// Reconciler uses a finalizer to ensure the release uninstall succeeds
// before CR deletion occurs.
//
// If an error occurs during release installation or upgrade, the change will be
// rolled back to restore the previous state.
//
// Reconcile also manages the status field of the custom resource. It includes
// the release name and manifest in `status.deployedRelease`, and it updates
// `status.conditions` based on reconciliation progress and success. Condition
// types include:
//
// - Deployed - a release for this CR is deployed (but not necessarily ready).
// - ReleaseFailed - an installation or upgrade failed.
// - Irreconcilable - an error occurred during reconciliation
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
log := r.log.WithValues(strings.ToLower(r.gvk.Kind), req.NamespacedName)
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(*r.gvk)
err = r.client.Get(ctx, req.NamespacedName, obj)
if apierrors.IsNotFound(err) {
return ctrl.Result{}, nil
}
if err != nil {
return ctrl.Result{}, err
}
u := updater.New(r.client)
defer func() {
applyErr := u.Apply(ctx, obj)
if err == nil && !apierrors.IsNotFound(applyErr) {
err = applyErr
}
}()
actionClient, err := r.actionClientGetter.ActionClientFor(obj)
if err != nil {
u.UpdateStatus(
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonErrorGettingClient, err)),
updater.EnsureConditionUnknown(conditions.TypeDeployed),
updater.EnsureConditionUnknown(conditions.TypeInitialized),
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
updater.EnsureDeployedRelease(nil),
)
// NOTE: If obj has the uninstall finalizer, that means a release WAS deployed at some point
// in the past, but we don't know if it still is because we don't have an actionClient to check.
// So the question is, what do we do with the finalizer? We could:
// - Leave it in place. This would make the CR impossible to delete without either resolving this error, or
// manually uninstalling the release, deleting the finalizer, and deleting the CR.
// - Remove the finalizer. This would make it possible to delete the CR, but it would leave around any
// release resources that are not owned by the CR (those in the cluster scope or in other namespaces).
//
// The decision made for now is to leave the finalizer in place, so that the user can intervene and try to
// resolve the issue, instead of the operator silently leaving some dangling resources hanging around after the
// CR is deleted.
return ctrl.Result{}, err
}
// As soon as we get the actionClient, lookup the release and
// update the status with this info. We need to do this as
// early as possible in case other irreconcilable errors occur.
//
// We also make sure not to return any errors we encounter so
// we can still attempt an uninstall if the CR is being deleted.
rel, err := actionClient.Get(obj.GetName())
if errors.Is(err, driver.ErrReleaseNotFound) {
u.UpdateStatus(updater.EnsureCondition(conditions.Deployed(corev1.ConditionFalse, "", "")))
} else if err == nil {
ensureDeployedRelease(&u, rel)
}
u.UpdateStatus(updater.EnsureCondition(conditions.Initialized(corev1.ConditionTrue, "", "")))
if obj.GetDeletionTimestamp() != nil {
err := r.handleDeletion(ctx, actionClient, obj, log)
return ctrl.Result{}, err
}
vals, err := r.getValues(obj)
if err != nil {
u.UpdateStatus(
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonErrorGettingValues, err)),
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
)
return ctrl.Result{}, err
}
rel, state, err := r.getReleaseState(actionClient, obj, vals.AsMap())
if err != nil {
u.UpdateStatus(
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonErrorGettingReleaseState, err)),
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
updater.EnsureConditionUnknown(conditions.TypeDeployed),
updater.EnsureDeployedRelease(nil),
)
return ctrl.Result{}, err
}
u.UpdateStatus(updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionFalse, "", "")))
for _, h := range r.preHooks {
if err := h.Exec(obj, vals, log); err != nil {
log.Error(err, "pre-release hook failed")
}
}
switch state {
case stateNeedsInstall:
rel, err = r.doInstall(actionClient, &u, obj, vals.AsMap(), log)
if err != nil {
return ctrl.Result{}, err
}
case stateNeedsUpgrade:
rel, err = r.doUpgrade(actionClient, &u, obj, vals.AsMap(), log)
if err != nil {
return ctrl.Result{}, err
}
case stateUnchanged:
if err := r.doReconcile(actionClient, &u, rel, log); err != nil {
return ctrl.Result{}, err
}
default:
return ctrl.Result{}, fmt.Errorf("unexpected release state: %s", state)
}
for _, h := range r.postHooks {
if err := h.Exec(obj, *rel, log); err != nil {
log.Error(err, "post-release hook failed", "name", rel.Name, "version", rel.Version)
}
}
ensureDeployedRelease(&u, rel)
u.UpdateStatus(
updater.EnsureCondition(conditions.ReleaseFailed(corev1.ConditionFalse, "", "")),
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionFalse, "", "")),
)
return ctrl.Result{RequeueAfter: r.reconcilePeriod}, nil
}
func (r *Reconciler) getValues(obj *unstructured.Unstructured) (chartutil.Values, error) {
crVals, err := internalvalues.FromUnstructured(obj)
if err != nil {
return chartutil.Values{}, err
}
if err := crVals.ApplyOverrides(r.overrideValues); err != nil {
return chartutil.Values{}, err
}
vals := r.valueMapper.Map(crVals.Map())
vals, err = chartutil.CoalesceValues(r.chrt, vals)
if err != nil {
return chartutil.Values{}, err
}
return vals, nil
}
type helmReleaseState string
const (
stateNeedsInstall helmReleaseState = "needs install"
stateNeedsUpgrade helmReleaseState = "needs upgrade"
stateUnchanged helmReleaseState = "unchanged"
stateError helmReleaseState = "error"
)
func (r *Reconciler) handleDeletion(ctx context.Context, actionClient helmclient.ActionInterface, obj *unstructured.Unstructured, log logr.Logger) error {
if !controllerutil.ContainsFinalizer(obj, uninstallFinalizer) {
log.Info("Resource is terminated, skipping reconciliation")
return nil
}
// Use defer in a closure so that it executes before we wait for
// the deletion of the CR. This might seem unnecessary since we're
// applying changes to the CR after is has a deletion timestamp.
// However, if uninstall fails, the finalizer will not be removed
// and we need to be able to update the conditions on the CR to
// indicate that the uninstall failed.
if err := func() (err error) {
uninstallUpdater := updater.New(r.client)
defer func() {
applyErr := uninstallUpdater.Apply(ctx, obj)
if err == nil {
err = applyErr
}
}()
return r.doUninstall(actionClient, &uninstallUpdater, obj, log)
}(); err != nil {
return err
}
// Since the client is hitting a cache, waiting for the
// deletion here will guarantee that the next reconciliation
// will see that the CR has been deleted and that there's
// nothing left to do.
if err := controllerutil.WaitForDeletion(ctx, r.client, obj); err != nil {
return err
}
return nil
}
func (r *Reconciler) getReleaseState(client helmclient.ActionInterface, obj metav1.Object, vals map[string]interface{}) (*release.Release, helmReleaseState, error) {
currentRelease, err := client.Get(obj.GetName())
if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) {
return nil, stateError, err
}
if errors.Is(err, driver.ErrReleaseNotFound) {
return nil, stateNeedsInstall, nil
}
var opts []helmclient.UpgradeOption
for name, annot := range r.upgradeAnnotations {
if v, ok := obj.GetAnnotations()[name]; ok {
opts = append(opts, annot.UpgradeOption(v))
}
}
opts = append(opts, func(u *action.Upgrade) error {
u.DryRun = true
return nil
})
specRelease, err := client.Upgrade(obj.GetName(), obj.GetNamespace(), r.chrt, vals, opts...)
if err != nil {
return currentRelease, stateError, err
}
if specRelease.Manifest != currentRelease.Manifest ||
currentRelease.Info.Status == release.StatusFailed ||
currentRelease.Info.Status == release.StatusSuperseded {
return currentRelease, stateNeedsUpgrade, nil
}
return currentRelease, stateUnchanged, nil
}
func (r *Reconciler) doInstall(actionClient helmclient.ActionInterface, u *updater.Updater, obj *unstructured.Unstructured, vals map[string]interface{}, log logr.Logger) (*release.Release, error) {
var opts []helmclient.InstallOption
for name, annot := range r.installAnnotations {
if v, ok := obj.GetAnnotations()[name]; ok {
opts = append(opts, annot.InstallOption(v))
}
}
rel, err := actionClient.Install(obj.GetName(), obj.GetNamespace(), r.chrt, vals, opts...)
if err != nil {
u.UpdateStatus(
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
updater.EnsureCondition(conditions.ReleaseFailed(corev1.ConditionTrue, conditions.ReasonInstallError, err)),
)
return nil, err
}
r.reportOverrideEvents(obj)
log.Info("Release installed", "name", rel.Name, "version", rel.Version)
return rel, nil
}
func (r *Reconciler) doUpgrade(actionClient helmclient.ActionInterface, u *updater.Updater, obj *unstructured.Unstructured, vals map[string]interface{}, log logr.Logger) (*release.Release, error) {
var opts []helmclient.UpgradeOption
for name, annot := range r.upgradeAnnotations {
if v, ok := obj.GetAnnotations()[name]; ok {
opts = append(opts, annot.UpgradeOption(v))
}
}
rel, err := actionClient.Upgrade(obj.GetName(), obj.GetNamespace(), r.chrt, vals, opts...)
if err != nil {
u.UpdateStatus(
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
updater.EnsureCondition(conditions.ReleaseFailed(corev1.ConditionTrue, conditions.ReasonUpgradeError, err)),
)
return nil, err
}
r.reportOverrideEvents(obj)
log.Info("Release upgraded", "name", rel.Name, "version", rel.Version)
return rel, nil
}
func (r *Reconciler) reportOverrideEvents(obj runtime.Object) {
for k, v := range r.overrideValues {
r.eventRecorder.Eventf(obj, "Warning", "ValueOverridden",
"Chart value %q overridden to %q by operator", k, v)
}
}
func (r *Reconciler) doReconcile(actionClient helmclient.ActionInterface, u *updater.Updater, rel *release.Release, log logr.Logger) error {
// If a change is made to the CR spec that causes a release failure, a
// ConditionReleaseFailed is added to the status conditions. If that change
// is then reverted to its previous state, the operator will stop
// attempting the release and will resume reconciling. In this case, we
// need to set the ConditionReleaseFailed to false because the failing
// release is no longer being attempted.
u.UpdateStatus(
updater.EnsureCondition(conditions.ReleaseFailed(corev1.ConditionFalse, "", "")),
)
if err := actionClient.Reconcile(rel); err != nil {
u.UpdateStatus(updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)))
return err
}
log.Info("Release reconciled", "name", rel.Name, "version", rel.Version)
return nil
}
func (r *Reconciler) doUninstall(actionClient helmclient.ActionInterface, u *updater.Updater, obj *unstructured.Unstructured, log logr.Logger) error {
var opts []helmclient.UninstallOption
for name, annot := range r.uninstallAnnotations {
if v, ok := obj.GetAnnotations()[name]; ok {
opts = append(opts, annot.UninstallOption(v))
}
}
resp, err := actionClient.Uninstall(obj.GetName(), opts...)
if errors.Is(err, driver.ErrReleaseNotFound) {
log.Info("Release not found, removing finalizer")
} else if err != nil {
u.UpdateStatus(
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
updater.EnsureCondition(conditions.ReleaseFailed(corev1.ConditionTrue, conditions.ReasonUninstallError, err)),
)
return err
} else {
log.Info("Release uninstalled", "name", resp.Release.Name, "version", resp.Release.Version)
}
u.Update(updater.RemoveFinalizer(uninstallFinalizer))
u.UpdateStatus(
updater.EnsureCondition(conditions.ReleaseFailed(corev1.ConditionFalse, "", "")),
updater.EnsureCondition(conditions.Deployed(corev1.ConditionFalse, conditions.ReasonUninstallSuccessful, "")),
updater.RemoveDeployedRelease(),
)
return nil
}
func (r *Reconciler) validate() error {
if r.gvk == nil {
return errors.New("gvk must not be nil")
}
if r.chrt == nil {
return errors.New("chart must not be nil")
}
return nil
}
func (r *Reconciler) addDefaults(mgr ctrl.Manager, controllerName string) {
if r.client == nil {
r.client = mgr.GetClient()
}
if r.log == nil {
r.log = ctrl.Log.WithName("controllers").WithName("Helm")
}
if r.actionClientGetter == nil {
actionConfigGetter := helmclient.NewActionConfigGetter(mgr.GetConfig(), mgr.GetRESTMapper(), r.log)
r.actionClientGetter = helmclient.NewActionClientGetter(actionConfigGetter)
}
if r.eventRecorder == nil {
r.eventRecorder = mgr.GetEventRecorderFor(controllerName)
}
if r.valueMapper == nil {
r.valueMapper = internalvalues.DefaultMapper
}
}
func (r *Reconciler) setupScheme(mgr ctrl.Manager) {
mgr.GetScheme().AddKnownTypeWithName(*r.gvk, &unstructured.Unstructured{})
metav1.AddToGroupVersion(mgr.GetScheme(), r.gvk.GroupVersion())
}
func (r *Reconciler) setupWatches(mgr ctrl.Manager, c controller.Controller) error {
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(*r.gvk)
if err := c.Watch(
&source.Kind{Type: obj},
&sdkhandler.InstrumentedEnqueueRequestForObject{},
); err != nil {
return err
}
secret := &corev1.Secret{}
secret.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Secret",
})
if err := c.Watch(
&source.Kind{Type: secret},
&handler.EnqueueRequestForOwner{
OwnerType: obj,
IsController: true,
},
); err != nil {
return err
}
if !r.skipDependentWatches {
r.postHooks = append([]hook.PostHook{internalhook.NewDependentResourceWatcher(c, mgr.GetRESTMapper())}, r.postHooks...)
}
return nil
}
func ensureDeployedRelease(u *updater.Updater, rel *release.Release) {
reason := conditions.ReasonInstallSuccessful
message := "release was successfully installed"
if rel.Version > 1 {
reason = conditions.ReasonUpgradeSuccessful
message = "release was successfully upgraded"
}
if rel.Info != nil && len(rel.Info.Notes) > 0 {
message = rel.Info.Notes
}
u.Update(updater.EnsureFinalizer(uninstallFinalizer))
u.UpdateStatus(
updater.EnsureCondition(conditions.Deployed(corev1.ConditionTrue, reason, message)),
updater.EnsureDeployedRelease(rel),
)
}

View File

@@ -0,0 +1,31 @@
/*
Copyright 2020 The Operator-SDK 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 values
import (
"helm.sh/helm/v3/pkg/chartutil"
)
type Mapper interface {
Map(chartutil.Values) chartutil.Values
}
type MapperFunc func(chartutil.Values) chartutil.Values
func (m MapperFunc) Map(v chartutil.Values) chartutil.Values {
return m(v)
}

View File

@@ -0,0 +1,107 @@
// Copyright 2020 The Operator-SDK 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 watches
import (
"errors"
"fmt"
"io/ioutil"
"os"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/yaml"
)
type Watch struct {
schema.GroupVersionKind `json:",inline"`
ChartPath string `json:"chart"`
WatchDependentResources *bool `json:"watchDependentResources,omitempty"`
OverrideValues map[string]string `json:"overrideValues,omitempty"`
ReconcilePeriod *metav1.Duration `json:"reconcilePeriod,omitempty"`
MaxConcurrentReconciles *int `json:"maxConcurrentReconciles,omitempty"`
Chart *chart.Chart `json:"-"`
}
// Load loads a slice of Watches from the watch file at `path`. For each entry
// in the watches file, it verifies the configuration. If an error is
// encountered loading the file or verifying the configuration, it will be
// returned.
func Load(path string) ([]Watch, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
watches := []Watch{}
err = yaml.Unmarshal(b, &watches)
if err != nil {
return nil, err
}
watchesMap := make(map[schema.GroupVersionKind]Watch)
for i, w := range watches {
if err := verifyGVK(w.GroupVersionKind); err != nil {
return nil, fmt.Errorf("invalid GVK: %s: %w", w.GroupVersionKind, err)
}
cl, err := loader.Load(w.ChartPath)
if err != nil {
return nil, fmt.Errorf("invalid chart %s: %w", w.ChartPath, err)
}
w.Chart = cl
w.OverrideValues = expandOverrideEnvs(w.OverrideValues)
if w.WatchDependentResources == nil {
trueVal := true
w.WatchDependentResources = &trueVal
}
if _, ok := watchesMap[w.GroupVersionKind]; ok {
return nil, fmt.Errorf("duplicate GVK: %s", w.GroupVersionKind)
}
watchesMap[w.GroupVersionKind] = w
watches[i] = w
}
return watches, nil
}
func expandOverrideEnvs(in map[string]string) map[string]string {
if in == nil {
return nil
}
out := make(map[string]string)
for k, v := range in {
out[k] = os.ExpandEnv(v)
}
return out
}
func verifyGVK(gvk schema.GroupVersionKind) error {
// A GVK without a group is valid. Certain scenarios may cause a GVK
// without a group to fail in other ways later in the initialization
// process.
if gvk.Version == "" {
return errors.New("version must not be empty")
}
if gvk.Kind == "" {
return errors.New("kind must not be empty")
}
return nil
}