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,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

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
}