diff --git a/internal/action/diff.go b/internal/action/diff.go index bbe49774a..f20dac538 100644 --- a/internal/action/diff.go +++ b/internal/action/diff.go @@ -24,7 +24,7 @@ import ( helmaction "helm.sh/helm/v3/pkg/action" helmrelease "helm.sh/helm/v3/pkg/release" "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/utils/pointer" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" @@ -42,7 +42,7 @@ func Diff(ctx context.Context, config *helmaction.Configuration, rls *helmreleas if err != nil { return nil, err } - c, err := client.New(cfg, client.Options{DryRun: pointer.Bool(true)}) + c, err := client.New(cfg, client.Options{DryRun: ptr.To(true)}) if err != nil { return nil, err } diff --git a/internal/action/verify.go b/internal/action/verify.go index db07cb545..3c6852260 100644 --- a/internal/action/verify.go +++ b/internal/action/verify.go @@ -90,19 +90,6 @@ func LastRelease(config *helmaction.Configuration, releaseName string) (*helmrel return rls, nil } -// IsInstalled returns true if there is any release in the Helm storage with the -// given name. It returns any error other than driver.ErrReleaseNotFound. -func IsInstalled(config *helmaction.Configuration, releaseName string) (bool, error) { - _, err := config.Releases.Last(release.ShortenName(releaseName)) - if err != nil { - if errors.Is(err, helmdriver.ErrReleaseNotFound) { - return false, nil - } - return false, err - } - return true, nil -} - // VerifySnapshot verifies the data of the given v2beta2.Snapshot // matches the release object in the Helm storage. It returns the verified // release, or an error of type ErrReleaseNotFound, ErrReleaseDisappeared, @@ -127,30 +114,6 @@ func VerifySnapshot(config *helmaction.Configuration, snapshot *v2.Snapshot) (rl return rls, nil } -// VerifyLastStorageItem verifies the data of the given v2beta2.Snapshot -// matches the last release object in the Helm storage. It returns the release -// and any verification error of type ErrReleaseNotFound, ErrReleaseDisappeared, -// ErrReleaseDigest or ErrReleaseNotObserved indicating the reason for the -// verification failure. -func VerifyLastStorageItem(config *helmaction.Configuration, snapshot *v2.Snapshot) (rls *helmrelease.Release, err error) { - if snapshot == nil { - return nil, ErrReleaseNotFound - } - - rls, err = config.Releases.Last(snapshot.Name) - if err != nil { - if errors.Is(err, helmdriver.ErrReleaseNotFound) { - return nil, ErrReleaseDisappeared - } - return nil, err - } - - if err = VerifyReleaseObject(snapshot, rls); err != nil { - return nil, err - } - return rls, nil -} - // VerifyReleaseObject verifies the data of the given v2beta2.Snapshot // matches the given Helm release object. It returns an error of type // ErrReleaseDigest or ErrReleaseNotObserved indicating the reason for the diff --git a/internal/action/verify_test.go b/internal/action/verify_test.go index d1118b4f9..4cc4dbb0e 100644 --- a/internal/action/verify_test.go +++ b/internal/action/verify_test.go @@ -225,70 +225,6 @@ func TestReleaseTargetChanged(t *testing.T) { } } -func TestIsInstalled(t *testing.T) { - var mockError = errors.New("query mock error") - - tests := []struct { - name string - releaseName string - releases []*helmrelease.Release - queryError error - want bool - wantErr error - }{ - { - name: "installed", - releaseName: "release", - releases: []*helmrelease.Release{ - testutil.BuildRelease(&helmrelease.MockReleaseOptions{ - Name: "release", - Version: 1, - Status: helmrelease.StatusDeployed, - Namespace: "default", - }), - }, - want: true, - }, - { - name: "not installed", - releaseName: "release", - want: false, - }, - { - name: "release list error", - queryError: mockError, - wantErr: mockError, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - s := helmstorage.Init(driver.NewMemory()) - for _, v := range tt.releases { - g.Expect(s.Create(v)).To(Succeed()) - } - - s.Driver = &storage.Failing{ - Driver: s.Driver, - QueryErr: tt.queryError, - } - - got, err := IsInstalled(&helmaction.Configuration{Releases: s}, tt.releaseName) - - if tt.wantErr != nil { - g.Expect(err).To(HaveOccurred()) - g.Expect(err).To(Equal(tt.wantErr)) - g.Expect(got).To(BeFalse()) - return - } - - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(got).To(Equal(tt.want)) - }) - } -} - func TestVerifySnapshot(t *testing.T) { mock := testutil.BuildRelease(&helmrelease.MockReleaseOptions{ Name: "release", @@ -372,89 +308,6 @@ func TestVerifySnapshot(t *testing.T) { } } -func TestVerifyLastStorageItem(t *testing.T) { - mockOne := testutil.BuildRelease(&helmrelease.MockReleaseOptions{ - Name: "release", - Version: 1, - Status: helmrelease.StatusSuperseded, - Namespace: "default", - }) - mockTwo := testutil.BuildRelease(&helmrelease.MockReleaseOptions{ - Name: "release", - Version: 2, - Status: helmrelease.StatusDeployed, - Namespace: "default", - }) - mockInfo := release.ObservedToSnapshot(release.ObserveRelease(mockTwo)) - mockQueryErr := errors.New("mock query error") - - tests := []struct { - name string - snapshot *v2.Snapshot - releases []*helmrelease.Release - queryError error - want *helmrelease.Release - wantErr error - }{ - { - name: "valid last release", - snapshot: mockInfo, - releases: []*helmrelease.Release{mockOne, mockTwo}, - want: mockTwo, - }, - { - name: "invalid last release", - snapshot: mockInfo, - releases: []*helmrelease.Release{mockOne}, - wantErr: ErrReleaseNotObserved, - }, - { - name: "no last release", - snapshot: mockInfo, - releases: []*helmrelease.Release{}, - wantErr: ErrReleaseDisappeared, - }, - { - name: "no release snapshot", - snapshot: nil, - releases: nil, - wantErr: ErrReleaseNotFound, - }, - { - name: "driver query error", - snapshot: mockInfo, - queryError: mockQueryErr, - wantErr: mockQueryErr, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := NewWithT(t) - - s := helmstorage.Init(driver.NewMemory()) - for _, v := range tt.releases { - g.Expect(s.Create(v)).To(Succeed()) - } - - s.Driver = &storage.Failing{ - Driver: s.Driver, - QueryErr: tt.queryError, - } - - rls, err := VerifyLastStorageItem(&helmaction.Configuration{Releases: s}, tt.snapshot) - if tt.wantErr != nil { - g.Expect(err).To(HaveOccurred()) - g.Expect(err).To(Equal(tt.wantErr)) - g.Expect(rls).To(BeNil()) - return - } - - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(rls).To(Equal(tt.want)) - }) - } -} - func TestVerifyReleaseObject(t *testing.T) { mockRls := testutil.BuildRelease(&helmrelease.MockReleaseOptions{ Name: "release", diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index 0744f7f0f..1c3c8f6da 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -549,6 +549,9 @@ func (r *HelmReleaseReconciler) adoptLegacyRelease(ctx context.Context, getter g action.WithStorage(action.DefaultStorageDriver, storageNamespace), action.WithStorageLog(action.NewDebugLog(ctrl.LoggerFrom(ctx).V(logger.TraceLevel))), ) + if err != nil { + return err + } // Get the last successful release based on the observation for the v2beta1 // object. diff --git a/internal/controller/helmrelease_controller_test.go b/internal/controller/helmrelease_controller_test.go index ce5d7c782..e432a210b 100644 --- a/internal/controller/helmrelease_controller_test.go +++ b/internal/controller/helmrelease_controller_test.go @@ -512,7 +512,7 @@ func TestHelmReleaseReconciler_reconcileRelease(t *testing.T) { Status: v2.HelmReleaseStatus{ HelmChart: chart.Namespace + "/" + chart.Name, LastReleaseRevision: rls.Version, - LastAttemptedValuesChecksum: valChecksum.Hex(), + LastAttemptedValuesChecksum: valChecksum.Encoded(), }, } diff --git a/internal/features/features.go b/internal/features/features.go index 4d39cad0f..10ce03edf 100644 --- a/internal/features/features.go +++ b/internal/features/features.go @@ -31,11 +31,15 @@ const ( // DetectDrift configures the detection of cluster state drift compared to // the desired state as described in the manifest of the Helm release // storage object. + // Deprecated in v0.37.0, use the drift detection mode on the HelmRelease + // object instead. DetectDrift = "DetectDrift" // CorrectDrift configures the correction of cluster state drift compared to // the desired state as described in the manifest of the Helm release. It // is only effective when DetectDrift is enabled. + // Deprecated in v0.37.0, use the drift detection mode on the HelmRelease + // object instead. CorrectDrift = "CorrectDrift" // AllowDNSLookups allows the controller to perform DNS lookups when rendering Helm @@ -61,11 +65,11 @@ var features = map[string]bool{ // opt-in from v0.28 CacheSecretsAndConfigMaps: false, // DetectDrift - // opt-in from v0.31 + // deprecated in v0.37.0 DetectDrift: false, // CorrectDrift, - // opt-out from v0.31.2 - CorrectDrift: true, + // deprecated in v0.37.0 + CorrectDrift: false, // AllowDNSLookups // opt-in from v0.31 AllowDNSLookups: false, diff --git a/internal/reconcile/atomic_release.go b/internal/reconcile/atomic_release.go index 5bebadc54..3949cc0b2 100644 --- a/internal/reconcile/atomic_release.go +++ b/internal/reconcile/atomic_release.go @@ -321,7 +321,7 @@ func (r *AtomicRelease) actionForState(ctx context.Context, req *Request, state log.Info(msgWithReason("detected changes in cluster state", diff.SummarizeDiffSetBrief(state.Diff))) for _, change := range state.Diff { if change.Type == jsondiff.DiffTypeCreate || change.Type == jsondiff.DiffTypeUpdate { - log.V(logger.DebugLevel).Info(fmt.Sprintf("observed change in cluster state"), "diff", change) + log.V(logger.DebugLevel).Info("observed change in cluster state", "diff", change) } } diff --git a/internal/reconcile/atomic_release_test.go b/internal/reconcile/atomic_release_test.go index 30673d682..a6cbcde1b 100644 --- a/internal/reconcile/atomic_release_test.go +++ b/internal/reconcile/atomic_release_test.go @@ -33,7 +33,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/tools/record" - "k8s.io/utils/pointer" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/fluxcd/pkg/apis/meta" @@ -616,7 +616,7 @@ func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) { spec: func(spec *v2.HelmReleaseSpec) { spec.Install = &v2.Install{ Remediation: &v2.InstallRemediation{ - RemediateLastFailure: pointer.Bool(true), + RemediateLastFailure: ptr.To(true), }, } spec.Uninstall = &v2.Uninstall{ @@ -635,7 +635,7 @@ func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) { spec: func(spec *v2.HelmReleaseSpec) { spec.Install = &v2.Install{ Remediation: &v2.InstallRemediation{ - RemediateLastFailure: pointer.Bool(true), + RemediateLastFailure: ptr.To(true), }, } spec.Test = &v2.Test{ @@ -740,7 +740,7 @@ func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) { spec: func(spec *v2.HelmReleaseSpec) { spec.Upgrade = &v2.Upgrade{ Remediation: &v2.UpgradeRemediation{ - RemediateLastFailure: pointer.Bool(true), + RemediateLastFailure: ptr.To(true), }, } }, @@ -778,7 +778,7 @@ func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) { spec.Upgrade = &v2.Upgrade{ Remediation: &v2.UpgradeRemediation{ Strategy: &strategy, - RemediateLastFailure: pointer.Bool(true), + RemediateLastFailure: ptr.To(true), }, } spec.Uninstall = &v2.Uninstall{ @@ -816,7 +816,7 @@ func TestAtomicRelease_Reconcile_Scenarios(t *testing.T) { spec: func(spec *v2.HelmReleaseSpec) { spec.Upgrade = &v2.Upgrade{ Remediation: &v2.UpgradeRemediation{ - RemediateLastFailure: pointer.Bool(true), + RemediateLastFailure: ptr.To(true), }, } spec.Test = &v2.Test{ diff --git a/internal/reconcile/state.go b/internal/reconcile/state.go index 0392dc2a9..e4b32594b 100644 --- a/internal/reconcile/state.go +++ b/internal/reconcile/state.go @@ -129,7 +129,7 @@ func DetermineReleaseState(ctx context.Context, cfg *action.ConfigFactory, req * case helmrelease.StatusFailed: return ReleaseState{Status: ReleaseStatusFailed}, nil case helmrelease.StatusUninstalled: - return ReleaseState{Status: ReleaseStatusAbsent, Reason: fmt.Sprintf("found uninstalled release in storage")}, nil + return ReleaseState{Status: ReleaseStatusAbsent, Reason: "found uninstalled release in storage"}, nil case helmrelease.StatusDeployed: // Verify the release is in sync with the desired configuration. if err = action.VerifyRelease(rls, cur, req.Chart.Metadata, req.Values); err != nil { diff --git a/internal/runner/log_buffer.go b/internal/runner/log_buffer.go deleted file mode 100644 index 3241c013b..000000000 --- a/internal/runner/log_buffer.go +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright 2021 The Flux 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 runner - -import ( - "container/ring" - "fmt" - "strings" - "sync" - - "github.com/go-logr/logr" - "helm.sh/helm/v3/pkg/action" -) - -const defaultBufferSize = 5 - -func NewDebugLog(log logr.Logger) action.DebugLog { - return func(format string, v ...interface{}) { - log.Info(fmt.Sprintf(format, v...)) - } -} - -type LogBuffer struct { - mu sync.Mutex - log action.DebugLog - buffer *ring.Ring -} - -func NewLogBuffer(log action.DebugLog, size int) *LogBuffer { - if size <= 0 { - size = defaultBufferSize - } - return &LogBuffer{ - log: log, - buffer: ring.New(size), - } -} - -func (l *LogBuffer) Log(format string, v ...interface{}) { - l.mu.Lock() - - // Filter out duplicate log lines, this happens for example when - // Helm is waiting on workloads to become ready. - msg := fmt.Sprintf(format, v...) - if prev := l.buffer.Prev(); prev.Value != msg { - l.buffer.Value = msg - l.buffer = l.buffer.Next() - } - - l.mu.Unlock() - l.log(format, v...) -} - -func (l *LogBuffer) Reset() { - l.mu.Lock() - l.buffer = ring.New(l.buffer.Len()) - l.mu.Unlock() -} - -func (l *LogBuffer) String() string { - var str string - l.mu.Lock() - l.buffer.Do(func(s interface{}) { - if s == nil { - return - } - str += s.(string) + "\n" - }) - l.mu.Unlock() - return strings.TrimSpace(str) -} diff --git a/internal/runner/log_buffer_test.go b/internal/runner/log_buffer_test.go deleted file mode 100644 index 829699e20..000000000 --- a/internal/runner/log_buffer_test.go +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright 2021 The Flux 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 runner - -import ( - "testing" - - "github.com/go-logr/logr" -) - -func TestLogBuffer_Log(t *testing.T) { - tests := []struct { - name string - size int - fill []string - wantCount int - want string - }{ - {name: "log", size: 2, fill: []string{"a", "b", "c"}, wantCount: 3, want: "b\nc"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var count int - l := NewLogBuffer(func(format string, v ...interface{}) { - count++ - }, tt.size) - for _, v := range tt.fill { - l.Log("%s", v) - } - if count != tt.wantCount { - t.Errorf("Inner Log() called %v times, want %v", count, tt.wantCount) - } - if got := l.String(); got != tt.want { - t.Errorf("String() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestLogBuffer_Reset(t *testing.T) { - bufferSize := 10 - l := NewLogBuffer(NewDebugLog(logr.Discard()), bufferSize) - - if got := l.buffer.Len(); got != bufferSize { - t.Errorf("Len() = %v, want %v", got, bufferSize) - } - - for _, v := range []string{"a", "b", "c"} { - l.Log("%s", v) - } - - if got := l.String(); got == "" { - t.Errorf("String() = empty") - } - - l.Reset() - - if got := l.buffer.Len(); got != bufferSize { - t.Errorf("Len() = %v after Reset(), want %v", got, bufferSize) - } - if got := l.String(); got != "" { - t.Errorf("String() != empty after Reset()") - } -} - -func TestLogBuffer_String(t *testing.T) { - tests := []struct { - name string - size int - fill []string - want string - }{ - {name: "empty buffer", fill: []string{}, want: ""}, - {name: "filled buffer", size: 2, fill: []string{"a", "b", "c"}, want: "b\nc"}, - {name: "duplicate buffer items", fill: []string{"b", "b", "b"}, want: "b"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := NewLogBuffer(NewDebugLog(logr.Discard()), tt.size) - for _, v := range tt.fill { - l.Log("%s", v) - } - if got := l.String(); got != tt.want { - t.Errorf("String() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/runner/runner.go b/internal/runner/runner.go deleted file mode 100644 index c6f9234b9..000000000 --- a/internal/runner/runner.go +++ /dev/null @@ -1,492 +0,0 @@ -/* -Copyright 2021 The Flux 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 runner - -import ( - "bytes" - "context" - "errors" - "fmt" - "sync" - "time" - - "github.com/go-logr/logr" - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chartutil" - "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" - apiextension "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/resource" - - 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/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - - runtimelogger "github.com/fluxcd/pkg/runtime/logger" - - v2 "github.com/fluxcd/helm-controller/api/v2beta1" - "github.com/fluxcd/helm-controller/internal/features" - intpostrender "github.com/fluxcd/helm-controller/internal/postrender" -) - -var accessor = meta.NewAccessor() - -type ActionError struct { - Err error - CapturedLogs string -} - -func (e ActionError) Error() string { - return e.Err.Error() -} - -func (e ActionError) Unwrap() error { - return e.Err -} - -// Runner represents a Helm action runner capable of performing Helm -// operations for a v2beta1.HelmRelease. -type Runner struct { - mu sync.Mutex - config *action.Configuration - logBuffer *LogBuffer -} - -// NewRunner constructs a new Runner configured to run Helm actions with the -// given genericclioptions.RESTClientGetter, and the release and storage -// namespace configured to the provided values. -func NewRunner(getter genericclioptions.RESTClientGetter, storageNamespace string, logger logr.Logger) (*Runner, error) { - runner := &Runner{ - logBuffer: NewLogBuffer(NewDebugLog(logger.V(runtimelogger.DebugLevel)), defaultBufferSize), - } - - // Default to the trace level logger for the Helm action configuration, - // to ensure storage logs are captured. - cfg := new(action.Configuration) - if err := cfg.Init(getter, storageNamespace, "secret", NewDebugLog(logger.V(runtimelogger.TraceLevel))); err != nil { - return nil, err - } - - // Override the logger used by the Helm actions and Kube client with the log buffer, - // which provides useful information in the event of an error. - cfg.Log = runner.logBuffer.Log - if kc, ok := cfg.KubeClient.(*kube.Client); ok { - kc.Log = runner.logBuffer.Log - } - runner.config = cfg - - return runner, nil -} - -// Create post renderer instances from HelmRelease and combine them into -// a single combined post renderer. -func postRenderers(hr v2.HelmRelease) (postrender.PostRenderer, error) { - renderers := make([]postrender.PostRenderer, 0) - for _, r := range hr.Spec.PostRenderers { - if r.Kustomize != nil { - renderers = append(renderers, &intpostrender.Kustomize{ - Patches: r.Kustomize.Patches, - PatchesStrategicMerge: r.Kustomize.PatchesStrategicMerge, - PatchesJSON6902: r.Kustomize.PatchesJSON6902, - Images: r.Kustomize.Images, - }) - } - } - renderers = append(renderers, intpostrender.NewOriginLabels(v2.GroupVersion.Group, hr.Namespace, hr.Name)) - if len(renderers) == 0 { - return nil, nil - } - return intpostrender.NewCombined(renderers...), nil -} - -// Install runs a Helm install action for the given v2beta1.HelmRelease. -func (r *Runner) Install(ctx context.Context, hr v2.HelmRelease, chart *chart.Chart, values chartutil.Values) (*release.Release, error) { - r.mu.Lock() - defer r.mu.Unlock() - defer r.logBuffer.Reset() - - install := action.NewInstall(r.config) - install.ReleaseName = hr.GetReleaseName() - install.Namespace = hr.GetReleaseNamespace() - install.Timeout = hr.Spec.GetInstall().GetTimeout(hr.GetTimeout()).Duration - install.Wait = !hr.Spec.GetInstall().DisableWait - install.WaitForJobs = !hr.Spec.GetInstall().DisableWaitForJobs - install.DisableHooks = hr.Spec.GetInstall().DisableHooks - install.DisableOpenAPIValidation = hr.Spec.GetInstall().DisableOpenAPIValidation - install.Replace = hr.Spec.GetInstall().Replace - install.SkipCRDs = true - install.Devel = true - - if hr.Spec.TargetNamespace != "" { - install.CreateNamespace = hr.Spec.GetInstall().CreateNamespace - } - - // If user opted-in to allow DNS lookups, enable it. - if allowDNS, _ := features.Enabled(features.AllowDNSLookups); allowDNS { - install.EnableDNS = allowDNS - } - - renderer, err := postRenderers(hr) - if err != nil { - return nil, wrapActionErr(r.logBuffer, err) - } - install.PostRenderer = renderer - - // If user opted-in to install (or replace) CRDs, install them first. - var legacyCRDsPolicy = v2.Create - if hr.Spec.GetInstall().SkipCRDs { - legacyCRDsPolicy = v2.Skip - } - cRDsPolicy, err := r.validateCRDsPolicy(hr.Spec.GetInstall().CRDs, legacyCRDsPolicy) - if err != nil { - return nil, wrapActionErr(r.logBuffer, err) - } - if cRDsPolicy != v2.Skip && len(chart.CRDObjects()) > 0 { - if err := r.applyCRDs(cRDsPolicy, chart, setOriginVisitor(hr.Namespace, hr.Name)); err != nil { - return nil, wrapActionErr(r.logBuffer, err) - } - } - - rel, err := install.RunWithContext(ctx, chart, values.AsMap()) - return rel, wrapActionErr(r.logBuffer, err) -} - -// Upgrade runs an Helm upgrade action for the given v2beta1.HelmRelease. -func (r *Runner) Upgrade(ctx context.Context, hr v2.HelmRelease, chart *chart.Chart, values chartutil.Values) (*release.Release, error) { - r.mu.Lock() - defer r.mu.Unlock() - defer r.logBuffer.Reset() - - upgrade := action.NewUpgrade(r.config) - upgrade.Namespace = hr.GetReleaseNamespace() - upgrade.ResetValues = !hr.Spec.GetUpgrade().PreserveValues - upgrade.ReuseValues = hr.Spec.GetUpgrade().PreserveValues - upgrade.MaxHistory = hr.GetMaxHistory() - upgrade.Timeout = hr.Spec.GetUpgrade().GetTimeout(hr.GetTimeout()).Duration - upgrade.Wait = !hr.Spec.GetUpgrade().DisableWait - upgrade.WaitForJobs = !hr.Spec.GetUpgrade().DisableWaitForJobs - upgrade.DisableHooks = hr.Spec.GetUpgrade().DisableHooks - upgrade.DisableOpenAPIValidation = hr.Spec.GetUpgrade().DisableOpenAPIValidation - upgrade.Force = hr.Spec.GetUpgrade().Force - upgrade.CleanupOnFail = hr.Spec.GetUpgrade().CleanupOnFail - upgrade.Devel = true - - // If user opted-in to allow DNS lookups, enable it. - if allowDNS, _ := features.Enabled(features.AllowDNSLookups); allowDNS { - upgrade.EnableDNS = allowDNS - } - - renderer, err := postRenderers(hr) - if err != nil { - return nil, wrapActionErr(r.logBuffer, err) - } - upgrade.PostRenderer = renderer - - // If user opted-in to upgrade CRDs, upgrade them first. - cRDsPolicy, err := r.validateCRDsPolicy(hr.Spec.GetUpgrade().CRDs, v2.Skip) - if err != nil { - return nil, wrapActionErr(r.logBuffer, err) - } - if cRDsPolicy != v2.Skip && len(chart.CRDObjects()) > 0 { - if err := r.applyCRDs(cRDsPolicy, chart, setOriginVisitor(hr.Namespace, hr.Name)); err != nil { - return nil, wrapActionErr(r.logBuffer, err) - } - } - - rel, err := upgrade.RunWithContext(ctx, hr.GetReleaseName(), chart, values.AsMap()) - return rel, wrapActionErr(r.logBuffer, err) -} - -func (r *Runner) validateCRDsPolicy(policy v2.CRDsPolicy, defaultValue v2.CRDsPolicy) (v2.CRDsPolicy, error) { - switch policy { - case "": - return defaultValue, nil - case v2.Skip: - break - case v2.Create: - break - case v2.CreateReplace: - break - default: - return policy, fmt.Errorf("invalid CRD policy '%s' defined in field CRDsPolicy, valid values are '%s', '%s' or '%s'", - policy, v2.Skip, v2.Create, v2.CreateReplace, - ) - } - return policy, nil -} - -type rootScoped struct{} - -func (*rootScoped) Name() meta.RESTScopeName { - return meta.RESTScopeNameRoot -} - -// This has been adapted from https://github.com/helm/helm/blob/v3.5.4/pkg/action/install.go#L127 -func (r *Runner) applyCRDs(policy v2.CRDsPolicy, chart *chart.Chart, visitorFunc ...resource.VisitorFunc) error { - r.config.Log("apply CRDs with policy %s", policy) - - // Collect all CRDs from all files in `crds` directory. - allCrds := make(kube.ResourceList, 0) - for _, obj := range chart.CRDObjects() { - // Read in the resources - res, err := r.config.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false) - if err != nil { - r.config.Log("failed to parse CRDs from %s: %s", obj.Name, err) - return fmt.Errorf("failed to parse CRDs from %s: %w", obj.Name, err) - } - allCrds = append(allCrds, res...) - } - - // Visit CRDs with any provided visitor functions. - for _, visitor := range visitorFunc { - if err := allCrds.Visit(visitor); err != nil { - return err - } - } - - var totalItems []*resource.Info - switch policy { - case v2.Skip: - break - case v2.Create: - for i := range allCrds { - if rr, err := r.config.KubeClient.Create(allCrds[i : i+1]); err != nil { - crdName := allCrds[i].Name - // If the error is CRD already exists, continue. - if apierrors.IsAlreadyExists(err) { - r.config.Log("CRD %s is already present. Skipping.", crdName) - if rr != nil && rr.Created != nil { - totalItems = append(totalItems, rr.Created...) - } - continue - } - r.config.Log("failed to create CRD %s: %s", crdName, err) - return fmt.Errorf("failed to create CRD %s: %w", crdName, err) - } else { - if rr != nil && rr.Created != nil { - totalItems = append(totalItems, rr.Created...) - } - } - } - case v2.CreateReplace: - config, err := r.config.RESTClientGetter.ToRESTConfig() - if err != nil { - r.config.Log("Error while creating Kubernetes client config: %s", err) - return err - } - clientset, err := apiextension.NewForConfig(config) - if err != nil { - r.config.Log("Error while creating Kubernetes clientset for apiextension: %s", err) - return err - } - client := clientset.ApiextensionsV1().CustomResourceDefinitions() - original := make(kube.ResourceList, 0) - // Note, we build the originals from the current set of CRDs - // and therefore this upgrade will never delete CRDs that existed in the former release - // but no longer exist in the current release. - for _, res := range allCrds { - if o, err := client.Get(context.TODO(), res.Name, metav1.GetOptions{}); err == nil && o != nil { - o.GetResourceVersion() - original = append(original, &resource.Info{ - Client: clientset.ApiextensionsV1().RESTClient(), - Mapping: &meta.RESTMapping{ - Resource: schema.GroupVersionResource{ - Group: "apiextensions.k8s.io", - Version: res.Mapping.GroupVersionKind.Version, - Resource: "customresourcedefinition", - }, - GroupVersionKind: schema.GroupVersionKind{ - Kind: "CustomResourceDefinition", - Group: "apiextensions.k8s.io", - Version: res.Mapping.GroupVersionKind.Version, - }, - Scope: &rootScoped{}, - }, - Namespace: o.ObjectMeta.Namespace, - Name: o.ObjectMeta.Name, - Object: o, - ResourceVersion: o.ObjectMeta.ResourceVersion, - }) - } else if !apierrors.IsNotFound(err) { - r.config.Log("failed to get CRD %s: %s", res.Name, err) - return err - } - } - // Send them to Kube - if rr, err := r.config.KubeClient.Update(original, allCrds, true); err != nil { - r.config.Log("failed to apply CRD %s", err) - return fmt.Errorf("failed to apply CRD: %w", err) - } else { - if rr != nil { - if rr.Created != nil { - totalItems = append(totalItems, rr.Created...) - } - if rr.Updated != nil { - totalItems = append(totalItems, rr.Updated...) - } - if rr.Deleted != nil { - totalItems = append(totalItems, rr.Deleted...) - } - } - } - } - - if len(totalItems) > 0 { - // Give time for the CRD to be recognized. - if err := r.config.KubeClient.Wait(totalItems, 60*time.Second); err != nil { - r.config.Log("Error waiting for items: %s", err) - return err - } - - // Clear the RESTMapper cache, since it will not have the new CRDs. - // Further invalidation of the client is done at a later stage by Helm - // when it gathers the server capabilities. - if m, err := r.config.RESTClientGetter.ToRESTMapper(); err == nil { - if rm, ok := m.(meta.ResettableRESTMapper); ok { - r.config.Log("Clearing REST mapper cache") - rm.Reset() - } - } - } - return nil -} - -// Test runs an Helm test action for the given v2beta1.HelmRelease. -func (r *Runner) Test(hr v2.HelmRelease) (*release.Release, error) { - r.mu.Lock() - defer r.mu.Unlock() - defer r.logBuffer.Reset() - - test := action.NewReleaseTesting(r.config) - test.Namespace = hr.GetReleaseNamespace() - test.Timeout = hr.Spec.GetTest().GetTimeout(hr.GetTimeout()).Duration - - rel, err := test.Run(hr.GetReleaseName()) - return rel, wrapActionErr(r.logBuffer, err) -} - -// Rollback runs an Helm rollback action for the given v2beta1.HelmRelease. -func (r *Runner) Rollback(hr v2.HelmRelease) error { - r.mu.Lock() - defer r.mu.Unlock() - defer r.logBuffer.Reset() - - rollback := action.NewRollback(r.config) - rollback.Timeout = hr.Spec.GetRollback().GetTimeout(hr.GetTimeout()).Duration - rollback.Wait = !hr.Spec.GetRollback().DisableWait - rollback.WaitForJobs = !hr.Spec.GetRollback().DisableWaitForJobs - rollback.DisableHooks = hr.Spec.GetRollback().DisableHooks - rollback.Force = hr.Spec.GetRollback().Force - rollback.Recreate = hr.Spec.GetRollback().Recreate - rollback.CleanupOnFail = hr.Spec.GetRollback().CleanupOnFail - - err := rollback.Run(hr.GetReleaseName()) - return wrapActionErr(r.logBuffer, err) -} - -// Uninstall runs an Helm uninstall action for the given v2beta1.HelmRelease. -func (r *Runner) Uninstall(hr v2.HelmRelease) error { - r.mu.Lock() - defer r.mu.Unlock() - defer r.logBuffer.Reset() - - uninstall := action.NewUninstall(r.config) - uninstall.Timeout = hr.Spec.GetUninstall().GetTimeout(hr.GetTimeout()).Duration - uninstall.DisableHooks = hr.Spec.GetUninstall().DisableHooks - uninstall.KeepHistory = hr.Spec.GetUninstall().KeepHistory - uninstall.DeletionPropagation = hr.Spec.GetUninstall().GetDeletionPropagation() - uninstall.Wait = !hr.Spec.GetUninstall().DisableWait - - _, err := uninstall.Run(hr.GetReleaseName()) - return wrapActionErr(r.logBuffer, err) -} - -// ObserveLastRelease observes the last revision, if there is one, -// for the actual Helm release associated with the given v2beta1.HelmRelease. -func (r *Runner) ObserveLastRelease(hr v2.HelmRelease) (*release.Release, error) { - rel, err := r.config.Releases.Last(hr.GetReleaseName()) - if err != nil && errors.Is(err, driver.ErrReleaseNotFound) { - err = nil - } - return rel, err -} - -func wrapActionErr(log *LogBuffer, err error) error { - if err == nil { - return err - } - err = &ActionError{ - Err: err, - CapturedLogs: log.String(), - } - return err -} - -func setOriginVisitor(namespace, name string) resource.VisitorFunc { - return func(info *resource.Info, err error) error { - if err != nil { - return err - } - if err = mergeLabels(info.Object, originLabels(name, namespace)); err != nil { - return fmt.Errorf( - "%s origin labels could not be updated: %s", - resourceString(info), err, - ) - } - return nil - } -} - -func mergeLabels(obj runtime.Object, labels map[string]string) error { - current, err := accessor.Labels(obj) - if err != nil { - return err - } - return accessor.SetLabels(obj, mergeStrStrMaps(current, labels)) -} - -func originLabels(name, namespace string) map[string]string { - return map[string]string{ - fmt.Sprintf("%s/name", v2.GroupVersion.Group): name, - fmt.Sprintf("%s/namespace", v2.GroupVersion.Group): namespace, - } -} - -func resourceString(info *resource.Info) string { - _, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind() - return fmt.Sprintf( - "%s %q in namespace %q", - k, info.Name, info.Namespace, - ) -} - -func mergeStrStrMaps(current, desired map[string]string) map[string]string { - result := make(map[string]string) - for k, v := range current { - result[k] = v - } - for k, desiredVal := range desired { - result[k] = desiredVal - } - return result -} diff --git a/internal/util/object.go b/internal/util/object.go deleted file mode 100644 index f6476fcdc..000000000 --- a/internal/util/object.go +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2023 The Flux authors -Copyright 2018 The Kubernetes 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. -*/ - -// TODO: Remove this when -// https://github.com/kubernetes-sigs/controller-runtime/blob/c783d2527a7da76332a2d8d563a6ca0b80c12122/pkg/client/apiutil/apimachinery.go#L76-L104 -// is included in a semver release. - -package util - -import ( - "errors" - "fmt" - - apimeta "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" -) - -// IsAPINamespaced returns true if the object is namespace scoped. -// For unstructured objects the gvk is found from the object itself. -func IsAPINamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper apimeta.RESTMapper) (bool, error) { - gvk, err := apiutil.GVKForObject(obj, scheme) - if err != nil { - return false, err - } - return IsAPINamespacedWithGVK(gvk, restmapper) -} - -// IsAPINamespacedWithGVK returns true if the object having the provided -// GVK is namespace scoped. -func IsAPINamespacedWithGVK(gk schema.GroupVersionKind, restmapper apimeta.RESTMapper) (bool, error) { - restmapping, err := restmapper.RESTMapping(schema.GroupKind{Group: gk.Group, Kind: gk.Kind}) - if err != nil { - return false, fmt.Errorf("failed to get restmapping: %w", err) - } - - scope := restmapping.Scope.Name() - - if scope == "" { - return false, errors.New("scope cannot be identified, empty scope returned") - } - - if scope != apimeta.RESTScopeNameRoot { - return true, nil - } - return false, nil -} diff --git a/main.go b/main.go index 1b2a63717..23ee93cf9 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,7 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" - "k8s.io/utils/pointer" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -204,7 +204,7 @@ func main() { }, }, Controller: ctrlcfg.Controller{ - RecoverPanic: pointer.Bool(true), + RecoverPanic: ptr.To(true), MaxConcurrentReconciles: concurrent, }, Metrics: metricsserver.Options{