updated vendor
This commit is contained in:
-125
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// DebugEnabledFunc is a function type that determines if debug logging is enabled
|
||||
// We use a function because we want to check the setting at log time, not when the logger is created
|
||||
type DebugEnabledFunc func() bool
|
||||
|
||||
// DebugCheckHandler checks settings.Debug at log time
|
||||
type DebugCheckHandler struct {
|
||||
handler slog.Handler
|
||||
debugEnabled DebugEnabledFunc
|
||||
}
|
||||
|
||||
// Enabled implements slog.Handler.Enabled
|
||||
func (h *DebugCheckHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
if level == slog.LevelDebug {
|
||||
if h.debugEnabled == nil {
|
||||
return false
|
||||
}
|
||||
return h.debugEnabled()
|
||||
}
|
||||
return true // Always log other levels
|
||||
}
|
||||
|
||||
// Handle implements slog.Handler.Handle
|
||||
func (h *DebugCheckHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
return h.handler.Handle(ctx, r)
|
||||
}
|
||||
|
||||
// WithAttrs implements slog.Handler.WithAttrs
|
||||
func (h *DebugCheckHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &DebugCheckHandler{
|
||||
handler: h.handler.WithAttrs(attrs),
|
||||
debugEnabled: h.debugEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
// WithGroup implements slog.Handler.WithGroup
|
||||
func (h *DebugCheckHandler) WithGroup(name string) slog.Handler {
|
||||
return &DebugCheckHandler{
|
||||
handler: h.handler.WithGroup(name),
|
||||
debugEnabled: h.debugEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLogger creates a new logger with dynamic debug checking
|
||||
func NewLogger(debugEnabled DebugEnabledFunc) *slog.Logger {
|
||||
// Create base handler that removes timestamps
|
||||
baseHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
// Always use LevelDebug here to allow all messages through
|
||||
// Our custom handler will do the filtering
|
||||
Level: slog.LevelDebug,
|
||||
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
|
||||
// Remove the time attribute
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
})
|
||||
|
||||
// Wrap with our dynamic debug-checking handler
|
||||
dynamicHandler := &DebugCheckHandler{
|
||||
handler: baseHandler,
|
||||
debugEnabled: debugEnabled,
|
||||
}
|
||||
|
||||
return slog.New(dynamicHandler)
|
||||
}
|
||||
|
||||
// LoggerSetterGetter is an interface that can set and get a logger
|
||||
type LoggerSetterGetter interface {
|
||||
// SetLogger sets a new slog.Handler
|
||||
SetLogger(newHandler slog.Handler)
|
||||
// Logger returns the slog.Logger created from the slog.Handler
|
||||
Logger() *slog.Logger
|
||||
}
|
||||
|
||||
type LogHolder struct {
|
||||
// logger is an atomic.Pointer[slog.Logger] to store the slog.Logger
|
||||
// We use atomic.Pointer for thread safety
|
||||
logger atomic.Pointer[slog.Logger]
|
||||
}
|
||||
|
||||
// Logger returns the logger for the LogHolder. If nil, returns slog.Default().
|
||||
func (l *LogHolder) Logger() *slog.Logger {
|
||||
if lg := l.logger.Load(); lg != nil {
|
||||
return lg
|
||||
}
|
||||
return slog.New(slog.DiscardHandler) // Should never be reached
|
||||
}
|
||||
|
||||
// SetLogger sets the logger for the LogHolder. If nil, sets the default logger.
|
||||
func (l *LogHolder) SetLogger(newHandler slog.Handler) {
|
||||
if newHandler == nil {
|
||||
l.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs
|
||||
return
|
||||
}
|
||||
l.logger.Store(slog.New(newHandler))
|
||||
}
|
||||
|
||||
// Ensure LogHolder implements LoggerSetterGetter
|
||||
var _ LoggerSetterGetter = &LogHolder{}
|
||||
+29
-8
@@ -19,6 +19,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -158,18 +159,27 @@ func LoadDir(dirname string) (Plugin, error) {
|
||||
return pm.CreatePlugin(dirname, m)
|
||||
}
|
||||
|
||||
// LoadAll loads all plugins found beneath the base directory.
|
||||
func LogIgnorePluginLoadErrorFilterFunc(pluginYAML string, err error) error {
|
||||
slog.Warn("failed to load plugin (ignoring)", slog.String("plugin_yaml", pluginYAML), slog.Any("error", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// errorFilterFunc is a function that can filter errors during plugin loading
|
||||
type ErrorFilterFunc func(string, error) error
|
||||
|
||||
// LoadAllDir load all plugins found beneath the base directory, using the provided error filter to determine whether to fail on individual plugin load errors.
|
||||
//
|
||||
// This scans only one directory level.
|
||||
func LoadAll(basedir string) ([]Plugin, error) {
|
||||
var plugins []Plugin
|
||||
// We want basedir/*/plugin.yaml
|
||||
func LoadAllDir(basedir string, errorFilter ErrorFilterFunc) ([]Plugin, error) {
|
||||
// We want <basedir>/*/plugin.yaml
|
||||
scanpath := filepath.Join(basedir, "*", PluginFileName)
|
||||
matches, err := filepath.Glob(scanpath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search for plugins in %q: %w", scanpath, err)
|
||||
}
|
||||
|
||||
plugins := make([]Plugin, 0, len(matches))
|
||||
|
||||
// empty dir should load
|
||||
if len(matches) == 0 {
|
||||
return plugins, nil
|
||||
@@ -179,9 +189,12 @@ func LoadAll(basedir string) ([]Plugin, error) {
|
||||
dir := filepath.Dir(yamlFile)
|
||||
p, err := LoadDir(dir)
|
||||
if err != nil {
|
||||
return plugins, err
|
||||
if errNew := errorFilter(yamlFile, err); errNew != nil {
|
||||
return plugins, errNew
|
||||
}
|
||||
} else {
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
return plugins, detectDuplicates(plugins)
|
||||
}
|
||||
@@ -193,8 +206,12 @@ type findFunc func(pluginsDir string) ([]Plugin, error)
|
||||
type filterFunc func(Plugin) bool
|
||||
|
||||
// FindPlugins returns a list of plugins that match the descriptor
|
||||
// Errors loading a plugin are ignored with a warning
|
||||
func FindPlugins(pluginsDirs []string, descriptor Descriptor) ([]Plugin, error) {
|
||||
return findPlugins(pluginsDirs, LoadAll, makeDescriptorFilter(descriptor))
|
||||
loadAllIgnoreErrors := func(pluginsDir string) ([]Plugin, error) {
|
||||
return LoadAllDir(pluginsDir, LogIgnorePluginLoadErrorFilterFunc)
|
||||
}
|
||||
return findPlugins(pluginsDirs, loadAllIgnoreErrors, makeDescriptorFilter(descriptor))
|
||||
}
|
||||
|
||||
// findPlugins is the internal implementation that uses the find and filter functions
|
||||
@@ -237,7 +254,11 @@ func makeDescriptorFilter(descriptor Descriptor) filterFunc {
|
||||
|
||||
// FindPlugin returns a single plugin that matches the descriptor
|
||||
func FindPlugin(dirs []string, descriptor Descriptor) (Plugin, error) {
|
||||
plugins, err := FindPlugins(dirs, descriptor)
|
||||
loadAllIgnoreErrors := func(pluginsDir string) ([]Plugin, error) {
|
||||
return LoadAllDir(pluginsDir, LogIgnorePluginLoadErrorFilterFunc)
|
||||
}
|
||||
|
||||
plugins, err := findPlugins(dirs, loadAllIgnoreErrors, makeDescriptorFilter(descriptor))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+18
-5
@@ -19,9 +19,17 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
|
||||
"helm.sh/helm/v4/internal/plugin/schema"
|
||||
)
|
||||
|
||||
// isValidSemver checks if the given string is a valid semantic version
|
||||
func isValidSemver(v string) bool {
|
||||
_, err := semver.StrictNewVersion(v)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Metadata of a plugin, converted from the "on-disk" legacy or v1 plugin.yaml
|
||||
// Specifically, Config and RuntimeConfig are converted to their respective types based on the plugin type and runtime
|
||||
type Metadata struct {
|
||||
@@ -57,24 +65,29 @@ func (m Metadata) Validate() error {
|
||||
errs = append(errs, fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name))
|
||||
}
|
||||
|
||||
// Require version to be valid semver if specified
|
||||
if m.Version != "" && !isValidSemver(m.Version) {
|
||||
errs = append(errs, fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version))
|
||||
}
|
||||
|
||||
if m.APIVersion == "" {
|
||||
errs = append(errs, fmt.Errorf("empty APIVersion"))
|
||||
errs = append(errs, errors.New("empty APIVersion"))
|
||||
}
|
||||
|
||||
if m.Type == "" {
|
||||
errs = append(errs, fmt.Errorf("empty type field"))
|
||||
errs = append(errs, errors.New("empty type field"))
|
||||
}
|
||||
|
||||
if m.Runtime == "" {
|
||||
errs = append(errs, fmt.Errorf("empty runtime field"))
|
||||
errs = append(errs, errors.New("empty runtime field"))
|
||||
}
|
||||
|
||||
if m.Config == nil {
|
||||
errs = append(errs, fmt.Errorf("missing config field"))
|
||||
errs = append(errs, errors.New("missing config field"))
|
||||
}
|
||||
|
||||
if m.RuntimeConfig == nil {
|
||||
errs = append(errs, fmt.Errorf("missing runtimeConfig field"))
|
||||
errs = append(errs, errors.New("missing runtimeConfig field"))
|
||||
}
|
||||
|
||||
// Validate the config itself
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
@@ -71,14 +72,19 @@ func (m *MetadataLegacy) Validate() error {
|
||||
if !validPluginName.MatchString(m.Name) {
|
||||
return fmt.Errorf("invalid plugin name %q: must contain only a-z, A-Z, 0-9, _ and -", m.Name)
|
||||
}
|
||||
|
||||
if m.Version != "" && !isValidSemver(m.Version) {
|
||||
return fmt.Errorf("invalid plugin version %q: must be valid semver", m.Version)
|
||||
}
|
||||
|
||||
m.Usage = sanitizeString(m.Usage)
|
||||
|
||||
if len(m.PlatformCommand) > 0 && len(m.Command) > 0 {
|
||||
return fmt.Errorf("both platformCommand and command are set")
|
||||
return errors.New("both platformCommand and command are set")
|
||||
}
|
||||
|
||||
if len(m.PlatformHooks) > 0 && len(m.Hooks) > 0 {
|
||||
return fmt.Errorf("both platformHooks and hooks are set")
|
||||
return errors.New("both platformHooks and hooks are set")
|
||||
}
|
||||
|
||||
// Validate downloader plugins
|
||||
|
||||
+11
-3
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@@ -48,7 +49,14 @@ type MetadataV1 struct {
|
||||
|
||||
func (m *MetadataV1) Validate() error {
|
||||
if !validPluginName.MatchString(m.Name) {
|
||||
return fmt.Errorf("invalid plugin `name`")
|
||||
return errors.New("invalid plugin `name`")
|
||||
}
|
||||
|
||||
if m.Version == "" {
|
||||
return errors.New("plugin `version` is required")
|
||||
}
|
||||
if !isValidSemver(m.Version) {
|
||||
return fmt.Errorf("invalid plugin `version` %q: must be valid semver", m.Version)
|
||||
}
|
||||
|
||||
if m.APIVersion != "v1" {
|
||||
@@ -56,11 +64,11 @@ func (m *MetadataV1) Validate() error {
|
||||
}
|
||||
|
||||
if m.Type == "" {
|
||||
return fmt.Errorf("`type` missing")
|
||||
return errors.New("`type` missing")
|
||||
}
|
||||
|
||||
if m.Runtime == "" {
|
||||
return fmt.Errorf("`runtime` missing")
|
||||
return errors.New("`runtime` missing")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -117,8 +117,8 @@ func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env
|
||||
cmd.Env = slices.Clone(os.Environ())
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
fmt.Sprintf("HELM_PLUGIN_NAME=%s", r.metadata.Name),
|
||||
fmt.Sprintf("HELM_PLUGIN_DIR=%s", r.pluginDir))
|
||||
"HELM_PLUGIN_NAME="+r.metadata.Name,
|
||||
"HELM_PLUGIN_DIR="+r.pluginDir)
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
|
||||
cmd.Stdin = stdin
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"helm.sh/helm/v4/internal/plugin/schema"
|
||||
)
|
||||
@@ -63,7 +64,7 @@ func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) {
|
||||
env["HELM_PLUGIN_DIR"] = r.pluginDir
|
||||
env["HELM_PLUGIN_USERNAME"] = msg.Options.Username
|
||||
env["HELM_PLUGIN_PASSWORD"] = msg.Options.Password
|
||||
env["HELM_PLUGIN_PASS_CREDENTIALS_ALL"] = fmt.Sprintf("%t", msg.Options.PassCredentialsAll)
|
||||
env["HELM_PLUGIN_PASS_CREDENTIALS_ALL"] = strconv.FormatBool(msg.Options.PassCredentialsAll)
|
||||
|
||||
command, args, err := PrepareCommands(d.PlatformCommand, false, []string{}, env)
|
||||
if err != nil {
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
@@ -55,7 +56,7 @@ type ConfigGetterV1 struct {
|
||||
|
||||
func (c *ConfigGetterV1) Validate() error {
|
||||
if len(c.Protocols) == 0 {
|
||||
return fmt.Errorf("getter has no protocols")
|
||||
return errors.New("getter has no protocols")
|
||||
}
|
||||
for i, protocol := range c.Protocols {
|
||||
if protocol == "" {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||
|
||||
"helm.sh/helm/v4/pkg/helmpath"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -80,7 +80,7 @@ func getPlatformCommand(cmds []PlatformCommand) ([]string, []string) {
|
||||
func PrepareCommands(cmds []PlatformCommand, expandArgs bool, extraArgs []string, env map[string]string) (string, []string, error) {
|
||||
cmdParts, args := getPlatformCommand(cmds)
|
||||
if len(cmdParts) == 0 || cmdParts[0] == "" {
|
||||
return "", nil, fmt.Errorf("no plugin command is applicable")
|
||||
return "", nil, errors.New("no plugin command is applicable")
|
||||
}
|
||||
envMappingFunc := func(key string) string {
|
||||
return env[key]
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ func VerifyPlugin(archiveData, provData []byte, filename, keyring string) (*prov
|
||||
return sig.Verify(archiveData, provData, filename)
|
||||
}
|
||||
|
||||
// isTarball checks if a file has a tarball extension
|
||||
// IsTarball checks if a file has a tarball extension
|
||||
func IsTarball(filename string) bool {
|
||||
return filepath.Ext(filename) == ".gz" || filepath.Ext(filename) == ".tgz"
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
This file was initially copied and modified from
|
||||
https://github.com/fluxcd/kustomize-controller/blob/main/internal/statusreaders/job.go
|
||||
Copyright 2022 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 statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
)
|
||||
|
||||
type customJobStatusReader struct {
|
||||
genericStatusReader engine.StatusReader
|
||||
}
|
||||
|
||||
func NewCustomJobStatusReader(mapper meta.RESTMapper) engine.StatusReader {
|
||||
genericStatusReader := statusreaders.NewGenericStatusReader(mapper, jobConditions)
|
||||
return &customJobStatusReader{
|
||||
genericStatusReader: genericStatusReader,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *customJobStatusReader) Supports(gk schema.GroupKind) bool {
|
||||
return gk == batchv1.SchemeGroupVersion.WithKind("Job").GroupKind()
|
||||
}
|
||||
|
||||
func (j *customJobStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) {
|
||||
return j.genericStatusReader.ReadStatus(ctx, reader, resource)
|
||||
}
|
||||
|
||||
func (j *customJobStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) {
|
||||
return j.genericStatusReader.ReadStatusForObject(ctx, reader, resource)
|
||||
}
|
||||
|
||||
// Ref: https://github.com/kubernetes-sigs/cli-utils/blob/v0.29.4/pkg/kstatus/status/core.go
|
||||
// Modified to return Current status only when the Job has completed as opposed to when it's in progress.
|
||||
func jobConditions(u *unstructured.Unstructured) (*status.Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
|
||||
parallelism := status.GetIntField(obj, ".spec.parallelism", 1)
|
||||
completions := status.GetIntField(obj, ".spec.completions", parallelism)
|
||||
succeeded := status.GetIntField(obj, ".status.succeeded", 0)
|
||||
failed := status.GetIntField(obj, ".status.failed", 0)
|
||||
|
||||
// Conditions
|
||||
// https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24
|
||||
objc, err := status.GetObjectWithConditions(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range objc.Status.Conditions {
|
||||
switch c.Type {
|
||||
case "Complete":
|
||||
if c.Status == corev1.ConditionTrue {
|
||||
message := fmt.Sprintf("Job Completed. succeeded: %d/%d", succeeded, completions)
|
||||
return &status.Result{
|
||||
Status: status.CurrentStatus,
|
||||
Message: message,
|
||||
Conditions: []status.Condition{},
|
||||
}, nil
|
||||
}
|
||||
case "Failed":
|
||||
message := fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions)
|
||||
if c.Status == corev1.ConditionTrue {
|
||||
return &status.Result{
|
||||
Status: status.FailedStatus,
|
||||
Message: message,
|
||||
Conditions: []status.Condition{
|
||||
{
|
||||
Type: status.ConditionStalled,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "JobFailed",
|
||||
Message: message,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message := "Job in progress"
|
||||
return &status.Result{
|
||||
Status: status.InProgressStatus,
|
||||
Message: message,
|
||||
Conditions: []status.Condition{
|
||||
{
|
||||
Type: status.ConditionReconciling,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "JobInProgress",
|
||||
Message: message,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 statusreaders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
)
|
||||
|
||||
type customPodStatusReader struct {
|
||||
genericStatusReader engine.StatusReader
|
||||
}
|
||||
|
||||
func NewCustomPodStatusReader(mapper meta.RESTMapper) engine.StatusReader {
|
||||
genericStatusReader := statusreaders.NewGenericStatusReader(mapper, podConditions)
|
||||
return &customPodStatusReader{
|
||||
genericStatusReader: genericStatusReader,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *customPodStatusReader) Supports(gk schema.GroupKind) bool {
|
||||
return gk == corev1.SchemeGroupVersion.WithKind("Pod").GroupKind()
|
||||
}
|
||||
|
||||
func (j *customPodStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) {
|
||||
return j.genericStatusReader.ReadStatus(ctx, reader, resource)
|
||||
}
|
||||
|
||||
func (j *customPodStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) {
|
||||
return j.genericStatusReader.ReadStatusForObject(ctx, reader, resource)
|
||||
}
|
||||
|
||||
func podConditions(u *unstructured.Unstructured) (*status.Result, error) {
|
||||
obj := u.UnstructuredContent()
|
||||
phase := status.GetStringField(obj, ".status.phase", "")
|
||||
switch corev1.PodPhase(phase) {
|
||||
case corev1.PodSucceeded:
|
||||
message := fmt.Sprintf("pod %s succeeded", u.GetName())
|
||||
return &status.Result{
|
||||
Status: status.CurrentStatus,
|
||||
Message: message,
|
||||
Conditions: []status.Condition{
|
||||
{
|
||||
Type: status.ConditionStalled,
|
||||
Status: corev1.ConditionTrue,
|
||||
Message: message,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case corev1.PodFailed:
|
||||
message := fmt.Sprintf("pod %s failed", u.GetName())
|
||||
return &status.Result{
|
||||
Status: status.FailedStatus,
|
||||
Message: message,
|
||||
Conditions: []status.Condition{
|
||||
{
|
||||
Type: status.ConditionStalled,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "PodFailed",
|
||||
Message: message,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
message := "Pod in progress"
|
||||
return &status.Result{
|
||||
Status: status.InProgressStatus,
|
||||
Message: message,
|
||||
Conditions: []status.Condition{
|
||||
{
|
||||
Type: status.ConditionReconciling,
|
||||
Status: corev1.ConditionTrue,
|
||||
Reason: "PodInProgress",
|
||||
Message: message,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,8 @@ func CopyFile(src, dst string) (err error) {
|
||||
//
|
||||
// ERROR_PRIVILEGE_NOT_HELD is 1314 (0x522):
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
|
||||
if lerr, ok := err.(*os.LinkError); ok && lerr.Err != syscall.Errno(1314) {
|
||||
lerr := &os.LinkError{}
|
||||
if errors.As(err, &lerr) && !errors.Is(lerr.Err, syscall.Errno(1314)) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -34,6 +34,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
@@ -46,10 +47,11 @@ func renameFallback(err error, src, dst string) error {
|
||||
// copy if we detect that case. syscall.EXDEV is the common name for the
|
||||
// cross device link error which has varying output text across different
|
||||
// operating systems.
|
||||
terr, ok := err.(*os.LinkError)
|
||||
terr := &os.LinkError{}
|
||||
ok := errors.As(err, &terr)
|
||||
if !ok {
|
||||
return err
|
||||
} else if terr.Err != syscall.EXDEV {
|
||||
} else if !errors.Is(terr.Err, syscall.EXDEV) {
|
||||
return fmt.Errorf("link error: cannot rename %s to %s: %w", src, dst, terr)
|
||||
}
|
||||
|
||||
|
||||
Vendored
-178
@@ -1,178 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 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.
|
||||
*/
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
apps "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
intstrutil "k8s.io/apimachinery/pkg/util/intstr"
|
||||
appsclient "k8s.io/client-go/kubernetes/typed/apps/v1"
|
||||
)
|
||||
|
||||
// deploymentutil contains a copy of a few functions from Kubernetes controller code to avoid a dependency on k8s.io/kubernetes.
|
||||
// This code is copied from https://github.com/kubernetes/kubernetes/blob/e856613dd5bb00bcfaca6974431151b5c06cbed5/pkg/controller/deployment/util/deployment_util.go
|
||||
// No changes to the code were made other than removing some unused functions
|
||||
|
||||
// RsListFunc returns the ReplicaSet from the ReplicaSet namespace and the List metav1.ListOptions.
|
||||
type RsListFunc func(string, metav1.ListOptions) ([]*apps.ReplicaSet, error)
|
||||
|
||||
// ListReplicaSets returns a slice of RSes the given deployment targets.
|
||||
// Note that this does NOT attempt to reconcile ControllerRef (adopt/orphan),
|
||||
// because only the controller itself should do that.
|
||||
// However, it does filter out anything whose ControllerRef doesn't match.
|
||||
func ListReplicaSets(deployment *apps.Deployment, getRSList RsListFunc) ([]*apps.ReplicaSet, error) {
|
||||
// TODO: Right now we list replica sets by their labels. We should list them by selector, i.e. the replica set's selector
|
||||
// should be a superset of the deployment's selector, see https://github.com/kubernetes/kubernetes/issues/19830.
|
||||
namespace := deployment.Namespace
|
||||
selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options := metav1.ListOptions{LabelSelector: selector.String()}
|
||||
all, err := getRSList(namespace, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Only include those whose ControllerRef matches the Deployment.
|
||||
owned := make([]*apps.ReplicaSet, 0, len(all))
|
||||
for _, rs := range all {
|
||||
if metav1.IsControlledBy(rs, deployment) {
|
||||
owned = append(owned, rs)
|
||||
}
|
||||
}
|
||||
return owned, nil
|
||||
}
|
||||
|
||||
// ReplicaSetsByCreationTimestamp sorts a list of ReplicaSet by creation timestamp, using their names as a tie breaker.
|
||||
type ReplicaSetsByCreationTimestamp []*apps.ReplicaSet
|
||||
|
||||
func (o ReplicaSetsByCreationTimestamp) Len() int { return len(o) }
|
||||
func (o ReplicaSetsByCreationTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
|
||||
func (o ReplicaSetsByCreationTimestamp) Less(i, j int) bool {
|
||||
if o[i].CreationTimestamp.Equal(&o[j].CreationTimestamp) {
|
||||
return o[i].Name < o[j].Name
|
||||
}
|
||||
return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp)
|
||||
}
|
||||
|
||||
// FindNewReplicaSet returns the new RS this given deployment targets (the one with the same pod template).
|
||||
func FindNewReplicaSet(deployment *apps.Deployment, rsList []*apps.ReplicaSet) *apps.ReplicaSet {
|
||||
sort.Sort(ReplicaSetsByCreationTimestamp(rsList))
|
||||
for i := range rsList {
|
||||
if EqualIgnoreHash(&rsList[i].Spec.Template, &deployment.Spec.Template) {
|
||||
// In rare cases, such as after cluster upgrades, Deployment may end up with
|
||||
// having more than one new ReplicaSets that have the same template as its template,
|
||||
// see https://github.com/kubernetes/kubernetes/issues/40415
|
||||
// We deterministically choose the oldest new ReplicaSet.
|
||||
return rsList[i]
|
||||
}
|
||||
}
|
||||
// new ReplicaSet does not exist.
|
||||
return nil
|
||||
}
|
||||
|
||||
// EqualIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash]
|
||||
// We ignore pod-template-hash because:
|
||||
// 1. The hash result would be different upon podTemplateSpec API changes
|
||||
// (e.g. the addition of a new field will cause the hash code to change)
|
||||
// 2. The deployment template won't have hash labels
|
||||
func EqualIgnoreHash(template1, template2 *v1.PodTemplateSpec) bool {
|
||||
t1Copy := template1.DeepCopy()
|
||||
t2Copy := template2.DeepCopy()
|
||||
// Remove hash labels from template.Labels before comparing
|
||||
delete(t1Copy.Labels, apps.DefaultDeploymentUniqueLabelKey)
|
||||
delete(t2Copy.Labels, apps.DefaultDeploymentUniqueLabelKey)
|
||||
return apiequality.Semantic.DeepEqual(t1Copy, t2Copy)
|
||||
}
|
||||
|
||||
// GetNewReplicaSet returns a replica set that matches the intent of the given deployment; get ReplicaSetList from client interface.
|
||||
// Returns nil if the new replica set doesn't exist yet.
|
||||
func GetNewReplicaSet(deployment *apps.Deployment, c appsclient.AppsV1Interface) (*apps.ReplicaSet, error) {
|
||||
rsList, err := ListReplicaSets(deployment, RsListFromClient(c))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FindNewReplicaSet(deployment, rsList), nil
|
||||
}
|
||||
|
||||
// RsListFromClient returns an rsListFunc that wraps the given client.
|
||||
func RsListFromClient(c appsclient.AppsV1Interface) RsListFunc {
|
||||
return func(namespace string, options metav1.ListOptions) ([]*apps.ReplicaSet, error) {
|
||||
rsList, err := c.ReplicaSets(namespace).List(context.Background(), options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ret []*apps.ReplicaSet
|
||||
for i := range rsList.Items {
|
||||
ret = append(ret, &rsList.Items[i])
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
}
|
||||
|
||||
// IsRollingUpdate returns true if the strategy type is a rolling update.
|
||||
func IsRollingUpdate(deployment *apps.Deployment) bool {
|
||||
return deployment.Spec.Strategy.Type == apps.RollingUpdateDeploymentStrategyType
|
||||
}
|
||||
|
||||
// MaxUnavailable returns the maximum unavailable pods a rolling deployment can take.
|
||||
func MaxUnavailable(deployment apps.Deployment) int32 {
|
||||
if !IsRollingUpdate(&deployment) || *(deployment.Spec.Replicas) == 0 {
|
||||
return int32(0)
|
||||
}
|
||||
// Error caught by validation
|
||||
_, maxUnavailable, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *(deployment.Spec.Replicas))
|
||||
if maxUnavailable > *deployment.Spec.Replicas {
|
||||
return *deployment.Spec.Replicas
|
||||
}
|
||||
return maxUnavailable
|
||||
}
|
||||
|
||||
// ResolveFenceposts resolves both maxSurge and maxUnavailable. This needs to happen in one
|
||||
// step. For example:
|
||||
//
|
||||
// 2 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1), then old(-1), then new(+1)
|
||||
// 1 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1)
|
||||
// 2 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
|
||||
// 1 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1)
|
||||
// 2 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
|
||||
// 1 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1)
|
||||
func ResolveFenceposts(maxSurge, maxUnavailable *intstrutil.IntOrString, desired int32) (int32, int32, error) {
|
||||
surge, err := intstrutil.GetValueFromIntOrPercent(intstrutil.ValueOrDefault(maxSurge, intstrutil.FromInt(0)), int(desired), true)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
unavailable, err := intstrutil.GetValueFromIntOrPercent(intstrutil.ValueOrDefault(maxUnavailable, intstrutil.FromInt(0)), int(desired), false)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if surge == 0 && unavailable == 0 {
|
||||
// Validation should never allow the user to explicitly use zero values for both maxSurge
|
||||
// maxUnavailable. Due to rounding down maxUnavailable though, it may resolve to zero.
|
||||
// If both fenceposts resolve to zero, then we should set maxUnavailable to 1 on the
|
||||
// theory that surge might not work due to quota.
|
||||
unavailable = 1
|
||||
}
|
||||
|
||||
return int32(surge), int32(unavailable), nil
|
||||
}
|
||||
+1
-1
@@ -112,7 +112,7 @@ func NewTLSConfig(options ...TLSConfigOption) (*tls.Config, error) {
|
||||
if len(to.caPEMBlock) > 0 {
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM(to.caPEMBlock) {
|
||||
return nil, fmt.Errorf("failed to append certificates from pem block")
|
||||
return nil, errors.New("failed to append certificates from pem block")
|
||||
}
|
||||
|
||||
config.RootCAs = cp
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
func K8sIOClientGoModVersion() (string, error) {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to read build info")
|
||||
return "", errors.New("failed to read build info")
|
||||
}
|
||||
|
||||
idx := slices.IndexFunc(info.Deps, func(m *debug.Module) bool {
|
||||
@@ -35,7 +35,7 @@ func K8sIOClientGoModVersion() (string, error) {
|
||||
})
|
||||
|
||||
if idx == -1 {
|
||||
return "", fmt.Errorf("k8s.io/client-go not found in build info")
|
||||
return "", errors.New("k8s.io/client-go not found in build info")
|
||||
}
|
||||
|
||||
m := info.Deps[idx]
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ var (
|
||||
//
|
||||
// Increment major number for new feature additions and behavioral changes.
|
||||
// Increment minor number for bug fixes and performance enhancements.
|
||||
version = "v4.1"
|
||||
version = "v4.2"
|
||||
|
||||
// metadata is extra build time data
|
||||
metadata = ""
|
||||
|
||||
+3
-3
@@ -157,7 +157,7 @@ func makeDefaultCapabilities() (*Capabilities, error) {
|
||||
|
||||
v, err := semver.NewVersion(vstr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse k8s.io/client-go version %q: %v", vstr, err)
|
||||
return nil, fmt.Errorf("unable to parse k8s.io/client-go version %q: %w", vstr, err)
|
||||
}
|
||||
|
||||
kubeVersionMajor := v.Major() + 1
|
||||
@@ -173,8 +173,8 @@ func newCapabilities(kubeVersionMajor, kubeVersionMinor uint64) (*Capabilities,
|
||||
KubeVersion: KubeVersion{
|
||||
Version: version,
|
||||
normalizedVersion: version,
|
||||
Major: fmt.Sprintf("%d", kubeVersionMajor),
|
||||
Minor: fmt.Sprintf("%d", kubeVersionMinor),
|
||||
Major: strconv.FormatUint(kubeVersionMajor, 10),
|
||||
Minor: strconv.FormatUint(kubeVersionMinor, 10),
|
||||
},
|
||||
APIVersions: DefaultVersionSet,
|
||||
HelmVersion: helmversion.Get(),
|
||||
|
||||
+9
-9
@@ -29,7 +29,7 @@ import (
|
||||
const GlobalKey = "global"
|
||||
|
||||
// Values represents a collection of chart values.
|
||||
type Values map[string]interface{}
|
||||
type Values map[string]any
|
||||
|
||||
// YAML encodes the Values into a YAML string.
|
||||
func (v Values) YAML() (string, error) {
|
||||
@@ -64,9 +64,9 @@ func (v Values) Table(name string) (Values, error) {
|
||||
// AsMap is a utility function for converting Values to a map[string]interface{}.
|
||||
//
|
||||
// It protects against nil map panics.
|
||||
func (v Values) AsMap() map[string]interface{} {
|
||||
func (v Values) AsMap() map[string]any {
|
||||
if len(v) == 0 {
|
||||
return map[string]interface{}{}
|
||||
return map[string]any{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func tableLookup(v Values, simple string) (Values, error) {
|
||||
if !ok {
|
||||
return v, ErrNoTable{simple}
|
||||
}
|
||||
if vv, ok := v2.(map[string]interface{}); ok {
|
||||
if vv, ok := v2.(map[string]any); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func ReadValues(data []byte) (vals Values, err error) {
|
||||
func ReadValuesFile(filename string) (Values, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
return map[string]any{}, err
|
||||
}
|
||||
return ReadValues(data)
|
||||
}
|
||||
@@ -129,8 +129,8 @@ type ReleaseOptions struct {
|
||||
}
|
||||
|
||||
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
|
||||
func istable(v interface{}) bool {
|
||||
_, ok := v.(map[string]interface{})
|
||||
func istable(v any) bool {
|
||||
_, ok := v.(map[string]any)
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -141,14 +141,14 @@ func istable(v interface{}) bool {
|
||||
// chapter:
|
||||
// one:
|
||||
// title: "Loomings"
|
||||
func (v Values) PathValue(path string) (interface{}, error) {
|
||||
func (v Values) PathValue(path string) (any, error) {
|
||||
if path == "" {
|
||||
return nil, errors.New("YAML path cannot be empty")
|
||||
}
|
||||
return v.pathValue(parsePath(path))
|
||||
}
|
||||
|
||||
func (v Values) pathValue(path []string) (interface{}, error) {
|
||||
func (v Values) pathValue(path []string) (any, error) {
|
||||
if len(path) == 1 {
|
||||
// if exists must be root key not table
|
||||
if _, ok := v[path[0]]; ok && !istable(v[path[0]]) {
|
||||
|
||||
@@ -172,7 +172,7 @@ func EnsureArchive(name string, raw *os.File) error {
|
||||
buffer := make([]byte, 512)
|
||||
_, err := raw.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
|
||||
return fmt.Errorf("file '%s' cannot be read: %w", name, err)
|
||||
}
|
||||
|
||||
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
|
||||
|
||||
+2
-2
@@ -48,11 +48,11 @@ type Chart struct {
|
||||
// Templates for this chart.
|
||||
Templates []*common.File `json:"templates"`
|
||||
// Values are default config for this chart.
|
||||
Values map[string]interface{} `json:"values"`
|
||||
Values map[string]any `json:"values"`
|
||||
// Schema is an optional JSON schema for imposing structure on Values
|
||||
Schema []byte `json:"schema"`
|
||||
// SchemaModTime the schema was last modified
|
||||
SchemaModTime time.Time `json:"schemamodtime,omitempty"`
|
||||
SchemaModTime time.Time `json:"schemamodtime"`
|
||||
// Files are miscellaneous files in a chart archive,
|
||||
// e.g. README, LICENSE, etc.
|
||||
Files []*common.File `json:"files"`
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ type Dependency struct {
|
||||
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
|
||||
// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
|
||||
// string or pair of child/parent sublist items.
|
||||
ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"`
|
||||
ImportValues []any `json:"import-values,omitempty" yaml:"import-values,omitempty"`
|
||||
// Alias usable alias to be used for the chart
|
||||
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,6 +25,6 @@ func (v ValidationError) Error() string {
|
||||
}
|
||||
|
||||
// ValidationErrorf takes a message and formatting options and creates a ValidationError
|
||||
func ValidationErrorf(msg string, args ...interface{}) ValidationError {
|
||||
func ValidationErrorf(msg string, args ...any) ValidationError {
|
||||
return ValidationError(fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
+9
-9
@@ -120,7 +120,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
|
||||
c.Metadata = new(chart.Metadata)
|
||||
}
|
||||
if c.Metadata.APIVersion != chart.APIVersionV1 {
|
||||
log.Printf("Warning: Dependencies are handled in Chart.yaml since apiVersion \"v2\". We recommend migrating dependencies to Chart.yaml.")
|
||||
log.Print("Warning: Dependencies are handled in Chart.yaml since apiVersion \"v2\". We recommend migrating dependencies to Chart.yaml.")
|
||||
}
|
||||
if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
|
||||
return c, fmt.Errorf("cannot load requirements.yaml: %w", err)
|
||||
@@ -138,7 +138,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
|
||||
c.Metadata = new(chart.Metadata)
|
||||
}
|
||||
if c.Metadata.APIVersion != chart.APIVersionV1 {
|
||||
log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.")
|
||||
log.Print("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.")
|
||||
}
|
||||
if c.Metadata.APIVersion == chart.APIVersionV1 {
|
||||
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data})
|
||||
@@ -209,11 +209,11 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
|
||||
//
|
||||
// The reader is expected to contain one or more YAML documents, the values of which are merged.
|
||||
// And the values can be either a chart's default values or user-supplied values.
|
||||
func LoadValues(data io.Reader) (map[string]interface{}, error) {
|
||||
values := map[string]interface{}{}
|
||||
func LoadValues(data io.Reader) (map[string]any, error) {
|
||||
values := map[string]any{}
|
||||
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
|
||||
for {
|
||||
currentMap := map[string]interface{}{}
|
||||
currentMap := map[string]any{}
|
||||
raw, err := reader.Read()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
@@ -231,13 +231,13 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
|
||||
|
||||
// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used.
|
||||
// If the value is a map, the maps will be merged recursively.
|
||||
func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(a))
|
||||
func MergeMaps(a, b map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(a))
|
||||
maps.Copy(out, a)
|
||||
for k, v := range b {
|
||||
if v, ok := v.(map[string]interface{}); ok {
|
||||
if v, ok := v.(map[string]any); ok {
|
||||
if bv, ok := out[k]; ok {
|
||||
if bv, ok := bv.(map[string]interface{}); ok {
|
||||
if bv, ok := bv.(map[string]any); ok {
|
||||
out[k] = MergeMaps(bv, v)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -112,6 +112,9 @@ func (md *Metadata) Validate() error {
|
||||
return ValidationError("chart.metadata.name is required")
|
||||
}
|
||||
|
||||
if md.Name == "." || md.Name == ".." {
|
||||
return ValidationErrorf("chart.metadata.name %q is not allowed", md.Name)
|
||||
}
|
||||
if md.Name != filepath.Base(md.Name) {
|
||||
return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
|
||||
}
|
||||
|
||||
+3
-4
@@ -24,7 +24,6 @@ These dependencies are expressed as interfaces so that alternate implementations
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -36,7 +35,7 @@ import (
|
||||
|
||||
"helm.sh/helm/v4/internal/version"
|
||||
"helm.sh/helm/v4/pkg/helmpath"
|
||||
"helm.sh/helm/v4/pkg/kube"
|
||||
"helm.sh/helm/v4/pkg/kubeenv"
|
||||
)
|
||||
|
||||
// defaultMaxHistory sets the maximum number of releases to 0: unlimited
|
||||
@@ -135,7 +134,7 @@ func New() *EnvSettings {
|
||||
config.Burst = env.BurstLimit
|
||||
config.QPS = env.QPS
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return &kube.RetryingRoundTripper{Wrapped: rt}
|
||||
return &kubeenv.RetryingRoundTripper{Wrapped: rt}
|
||||
})
|
||||
config.UserAgent = version.GetUserAgent()
|
||||
return config
|
||||
@@ -246,7 +245,7 @@ func (s *EnvSettings) EnvVars() map[string]string {
|
||||
"HELM_CACHE_HOME": helmpath.CachePath(""),
|
||||
"HELM_CONFIG_HOME": helmpath.ConfigPath(""),
|
||||
"HELM_DATA_HOME": helmpath.DataPath(""),
|
||||
"HELM_DEBUG": fmt.Sprint(s.Debug),
|
||||
"HELM_DEBUG": strconv.FormatBool(s.Debug),
|
||||
"HELM_PLUGINS": s.PluginsDirectory,
|
||||
"HELM_REGISTRY_CONFIG": s.RegistryConfig,
|
||||
"HELM_REPOSITORY_CACHE": s.RepositoryCache,
|
||||
|
||||
+46
-40
@@ -20,6 +20,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
@@ -37,13 +38,15 @@ type HTTPGetter struct {
|
||||
|
||||
// Get performs a Get from repo.Getter and returns the body.
|
||||
func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) {
|
||||
// Create a local copy of options to avoid data races when Get is called concurrently
|
||||
opts := g.opts
|
||||
for _, opt := range options {
|
||||
opt(&g.opts)
|
||||
opt(&opts)
|
||||
}
|
||||
return g.get(href)
|
||||
return g.get(href, opts)
|
||||
}
|
||||
|
||||
func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) {
|
||||
func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) {
|
||||
// Set a helm specific user agent so that a repo server and metrics can
|
||||
// separate helm calls from other tools interacting with repos.
|
||||
req, err := http.NewRequest(http.MethodGet, href, nil)
|
||||
@@ -51,18 +54,18 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if g.opts.acceptHeader != "" {
|
||||
req.Header.Set("Accept", g.opts.acceptHeader)
|
||||
if opts.acceptHeader != "" {
|
||||
req.Header.Set("Accept", opts.acceptHeader)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", version.GetUserAgent())
|
||||
if g.opts.userAgent != "" {
|
||||
req.Header.Set("User-Agent", g.opts.userAgent)
|
||||
if opts.userAgent != "" {
|
||||
req.Header.Set("User-Agent", opts.userAgent)
|
||||
}
|
||||
|
||||
// Before setting the basic auth credentials, make sure the URL associated
|
||||
// with the basic auth is the one being fetched.
|
||||
u1, err := url.Parse(g.opts.url)
|
||||
u1, err := url.Parse(opts.url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse getter URL: %w", err)
|
||||
}
|
||||
@@ -74,22 +77,24 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) {
|
||||
// Host on URL (returned from url.Parse) contains the port if present.
|
||||
// This check ensures credentials are not passed between different
|
||||
// services on different ports.
|
||||
if g.opts.passCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
|
||||
if g.opts.username != "" && g.opts.password != "" {
|
||||
req.SetBasicAuth(g.opts.username, g.opts.password)
|
||||
if opts.passCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
|
||||
if opts.username != "" && opts.password != "" {
|
||||
req.SetBasicAuth(opts.username, opts.password)
|
||||
}
|
||||
}
|
||||
|
||||
client, err := g.httpClient()
|
||||
client, err := g.httpClient(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slog.Debug("fetching", "url", href)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
slog.Debug("fetch complete", "url", href, "status", resp.Status, "content-length", resp.ContentLength)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to fetch %s : %s", href, resp.Status)
|
||||
}
|
||||
@@ -110,51 +115,52 @@ func NewHTTPGetter(options ...Option) (Getter, error) {
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func (g *HTTPGetter) httpClient() (*http.Client, error) {
|
||||
if g.opts.transport != nil {
|
||||
func (g *HTTPGetter) httpClient(opts getterOptions) (*http.Client, error) {
|
||||
if opts.transport != nil {
|
||||
return &http.Client{
|
||||
Transport: g.opts.transport,
|
||||
Timeout: g.opts.timeout,
|
||||
Transport: opts.transport,
|
||||
Timeout: opts.timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
g.once.Do(func() {
|
||||
g.transport = &http.Transport{
|
||||
// Check if we need custom TLS configuration
|
||||
needsCustomTLS := (opts.certFile != "" && opts.keyFile != "") || opts.caFile != "" || opts.insecureSkipVerifyTLS
|
||||
|
||||
if needsCustomTLS {
|
||||
// Create a new transport for custom TLS to avoid race conditions
|
||||
transport := &http.Transport{
|
||||
DisableCompression: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
// Being nil would cause the tls.Config default to be used
|
||||
// "NewTLSConfig" modifies an empty TLS config, not the default one
|
||||
TLSClientConfig: &tls.Config{},
|
||||
}
|
||||
})
|
||||
|
||||
if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" || g.opts.insecureSkipVerifyTLS {
|
||||
tlsConf, err := tlsutil.NewTLSConfig(
|
||||
tlsutil.WithInsecureSkipVerify(g.opts.insecureSkipVerifyTLS),
|
||||
tlsutil.WithCertKeyPairFiles(g.opts.certFile, g.opts.keyFile),
|
||||
tlsutil.WithCAFile(g.opts.caFile),
|
||||
tlsutil.WithInsecureSkipVerify(opts.insecureSkipVerifyTLS),
|
||||
tlsutil.WithCertKeyPairFiles(opts.certFile, opts.keyFile),
|
||||
tlsutil.WithCAFile(opts.caFile),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't create TLS config for client: %w", err)
|
||||
}
|
||||
|
||||
g.transport.TLSClientConfig = tlsConf
|
||||
transport.TLSClientConfig = tlsConf
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: opts.timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if g.opts.insecureSkipVerifyTLS {
|
||||
if g.transport.TLSClientConfig == nil {
|
||||
g.transport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
} else {
|
||||
g.transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
// Use shared transport for default case (no custom TLS)
|
||||
g.once.Do(func() {
|
||||
g.transport = &http.Transport{
|
||||
DisableCompression: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
client := &http.Client{
|
||||
return &http.Client{
|
||||
Transport: g.transport,
|
||||
Timeout: g.opts.timeout,
|
||||
}
|
||||
|
||||
return client, nil
|
||||
Timeout: opts.timeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) {
|
||||
client = c
|
||||
}
|
||||
|
||||
ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme))
|
||||
ref := strings.TrimPrefix(href, registry.OCIScheme+"://")
|
||||
|
||||
if version := g.opts.version; version != "" && !strings.Contains(path.Base(ref), ":") {
|
||||
ref = fmt.Sprintf("%s:%s", ref, version)
|
||||
|
||||
-1297
File diff suppressed because it is too large
Load Diff
-69
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube // import "helm.sh/helm/v4/pkg/kube"
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
var k8sNativeScheme *runtime.Scheme
|
||||
var k8sNativeSchemeOnce sync.Once
|
||||
|
||||
// AsVersioned converts the given info into a runtime.Object with the correct
|
||||
// group and version set
|
||||
func AsVersioned(info *resource.Info) runtime.Object {
|
||||
return convertWithMapper(info.Object, info.Mapping)
|
||||
}
|
||||
|
||||
// convertWithMapper converts the given object with the optional provided
|
||||
// RESTMapping. If no mapping is provided, the default schema versioner is used
|
||||
func convertWithMapper(obj runtime.Object, mapping *meta.RESTMapping) runtime.Object {
|
||||
s := kubernetesNativeScheme()
|
||||
var gv = runtime.GroupVersioner(schema.GroupVersions(s.PrioritizedVersionsAllGroups()))
|
||||
if mapping != nil {
|
||||
gv = mapping.GroupVersionKind.GroupVersion()
|
||||
}
|
||||
if obj, err := runtime.ObjectConvertor(s).ConvertToVersion(obj, gv); err == nil {
|
||||
return obj
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// kubernetesNativeScheme returns a clean *runtime.Scheme with _only_ Kubernetes
|
||||
// native resources added to it. This is required to break free of custom resources
|
||||
// that may have been added to scheme.Scheme due to Helm being used as a package in
|
||||
// combination with e.g. a versioned kube client. If we would not do this, the client
|
||||
// may attempt to perform e.g. a 3-way-merge strategy patch for custom resources.
|
||||
func kubernetesNativeScheme() *runtime.Scheme {
|
||||
k8sNativeSchemeOnce.Do(func() {
|
||||
k8sNativeScheme = runtime.NewScheme()
|
||||
scheme.AddToScheme(k8sNativeScheme)
|
||||
// API extensions are not in the above scheme set,
|
||||
// and must thus be added separately.
|
||||
apiextensionsv1beta1.AddToScheme(k8sNativeScheme)
|
||||
apiextensionsv1.AddToScheme(k8sNativeScheme)
|
||||
})
|
||||
return k8sNativeScheme
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube // import "helm.sh/helm/v4/pkg/kube"
|
||||
|
||||
import (
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/kubectl/pkg/validation"
|
||||
)
|
||||
|
||||
// Factory provides abstractions that allow the Kubectl command to be extended across multiple types
|
||||
// of resources and different API sets.
|
||||
// This interface is a minimal copy of the kubectl Factory interface containing only the functions
|
||||
// needed by Helm. Since Kubernetes Go APIs, including interfaces, can change in any minor release
|
||||
// this interface is not covered by the Helm backwards compatibility guarantee. The reasons for the
|
||||
// minimal copy is that it does not include the full interface. Changes or additions to functions
|
||||
// Helm does not need are not impacted or exposed. This minimizes the impact of Kubernetes changes
|
||||
// being exposed.
|
||||
type Factory interface {
|
||||
// ToRESTConfig returns restconfig
|
||||
ToRESTConfig() (*rest.Config, error)
|
||||
|
||||
// ToRawKubeConfigLoader return kubeconfig loader as-is
|
||||
ToRawKubeConfigLoader() clientcmd.ClientConfig
|
||||
|
||||
// DynamicClient returns a dynamic client ready for use
|
||||
DynamicClient() (dynamic.Interface, error)
|
||||
|
||||
// KubernetesClientSet gives you back an external clientset
|
||||
KubernetesClientSet() (*kubernetes.Clientset, error)
|
||||
|
||||
// NewBuilder returns an object that assists in loading objects from both disk and the server
|
||||
// and which implements the common patterns for CLI interactions with generic resources.
|
||||
NewBuilder() *resource.Builder
|
||||
|
||||
// Returns a schema that can validate objects stored on disk.
|
||||
Validator(validationDirective string) (validation.Schema, error)
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// Interface represents a client capable of communicating with the Kubernetes API.
|
||||
//
|
||||
// A KubernetesClient must be concurrency safe.
|
||||
type Interface interface {
|
||||
// Get details of deployed resources.
|
||||
// The first argument is a list of resources to get. The second argument
|
||||
// specifies if related pods should be fetched. For example, the pods being
|
||||
// managed by a deployment.
|
||||
Get(resources ResourceList, related bool) (map[string][]runtime.Object, error)
|
||||
|
||||
// Create creates one or more resources.
|
||||
Create(resources ResourceList, options ...ClientCreateOption) (*Result, error)
|
||||
|
||||
// Delete destroys one or more resources using the specified deletion propagation policy.
|
||||
// The 'policy' parameter determines how child resources are handled during deletion.
|
||||
Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error)
|
||||
|
||||
// Update updates one or more resources or creates the resource
|
||||
// if it doesn't exist.
|
||||
Update(original, target ResourceList, options ...ClientUpdateOption) (*Result, error)
|
||||
|
||||
// Build creates a resource list from a Reader.
|
||||
//
|
||||
// Reader must contain a YAML stream (one or more YAML documents separated
|
||||
// by "\n---\n")
|
||||
//
|
||||
// Validates against OpenAPI schema if validate is true.
|
||||
Build(reader io.Reader, validate bool) (ResourceList, error)
|
||||
// IsReachable checks whether the client is able to connect to the cluster.
|
||||
IsReachable() error
|
||||
|
||||
// GetWaiter gets the Kube.Waiter.
|
||||
GetWaiter(ws WaitStrategy) (Waiter, error)
|
||||
|
||||
// GetPodList lists all pods that match the specified listOptions
|
||||
GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error)
|
||||
|
||||
// OutputContainerLogsForPodList outputs the logs for a pod list
|
||||
OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error
|
||||
|
||||
// BuildTable creates a resource list from a Reader. This differs from
|
||||
// Interface.Build() in that a table kind is returned. A table is useful
|
||||
// if you want to use a printer to display the information.
|
||||
//
|
||||
// Reader must contain a YAML stream (one or more YAML documents separated
|
||||
// by "\n---\n")
|
||||
//
|
||||
// Validates against OpenAPI schema if validate is true.
|
||||
// TODO Helm 4: Integrate into Build with an argument
|
||||
BuildTable(reader io.Reader, validate bool) (ResourceList, error)
|
||||
}
|
||||
|
||||
// Waiter defines methods related to waiting for resource states.
|
||||
type Waiter interface {
|
||||
// Wait waits up to the given timeout for the specified resources to be ready.
|
||||
Wait(resources ResourceList, timeout time.Duration) error
|
||||
|
||||
// WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs.
|
||||
WaitWithJobs(resources ResourceList, timeout time.Duration) error
|
||||
|
||||
// WaitForDelete wait up to the given timeout for the specified resources to be deleted.
|
||||
WaitForDelete(resources ResourceList, timeout time.Duration) error
|
||||
|
||||
// WatchUntilReady watches the resources given and waits until it is ready.
|
||||
//
|
||||
// This method is mainly for hook implementations. It watches for a resource to
|
||||
// hit a particular milestone. The milestone depends on the Kind.
|
||||
//
|
||||
// For Jobs, "ready" means the Job ran to completion (exited without error).
|
||||
// For Pods, "ready" means the Pod phase is marked "succeeded".
|
||||
// For all other kinds, it means the kind was created or modified without
|
||||
// error.
|
||||
WatchUntilReady(resources ResourceList, timeout time.Duration) error
|
||||
}
|
||||
|
||||
// InterfaceWaitOptions defines an interface that extends Interface with
|
||||
// methods that accept wait options.
|
||||
//
|
||||
// TODO Helm 5: Remove InterfaceWaitOptions and integrate its method(s) into the Interface.
|
||||
type InterfaceWaitOptions interface {
|
||||
// GetWaiter gets the Kube.Waiter with options.
|
||||
GetWaiterWithOptions(ws WaitStrategy, opts ...WaitOption) (Waiter, error)
|
||||
}
|
||||
|
||||
var _ InterfaceWaitOptions = (*Client)(nil)
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
)
|
||||
|
||||
// WaitOption is a function that configures an option for waiting on resources.
|
||||
type WaitOption func(*waitOptions)
|
||||
|
||||
// WithWaitContext sets the context for waiting on resources.
|
||||
// If unset, context.Background() will be used.
|
||||
func WithWaitContext(ctx context.Context) WaitOption {
|
||||
return func(wo *waitOptions) {
|
||||
wo.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithWatchUntilReadyMethodContext sets the context specifically for the WatchUntilReady method.
|
||||
// If unset, the context set by `WithWaitContext` will be used (falling back to `context.Background()`).
|
||||
func WithWatchUntilReadyMethodContext(ctx context.Context) WaitOption {
|
||||
return func(wo *waitOptions) {
|
||||
wo.watchUntilReadyCtx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithWaitMethodContext sets the context specifically for the Wait method.
|
||||
// If unset, the context set by `WithWaitContext` will be used (falling back to `context.Background()`).
|
||||
func WithWaitMethodContext(ctx context.Context) WaitOption {
|
||||
return func(wo *waitOptions) {
|
||||
wo.waitCtx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithWaitWithJobsMethodContext sets the context specifically for the WaitWithJobs method.
|
||||
// If unset, the context set by `WithWaitContext` will be used (falling back to `context.Background()`).
|
||||
func WithWaitWithJobsMethodContext(ctx context.Context) WaitOption {
|
||||
return func(wo *waitOptions) {
|
||||
wo.waitWithJobsCtx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithWaitForDeleteMethodContext sets the context specifically for the WaitForDelete method.
|
||||
// If unset, the context set by `WithWaitContext` will be used (falling back to `context.Background()`).
|
||||
func WithWaitForDeleteMethodContext(ctx context.Context) WaitOption {
|
||||
return func(wo *waitOptions) {
|
||||
wo.waitForDeleteCtx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithKStatusReaders sets the status readers to be used while waiting on resources.
|
||||
func WithKStatusReaders(readers ...engine.StatusReader) WaitOption {
|
||||
return func(wo *waitOptions) {
|
||||
wo.statusReaders = readers
|
||||
}
|
||||
}
|
||||
|
||||
type waitOptions struct {
|
||||
ctx context.Context
|
||||
watchUntilReadyCtx context.Context
|
||||
waitCtx context.Context
|
||||
waitWithJobsCtx context.Context
|
||||
waitForDeleteCtx context.Context
|
||||
statusReaders []engine.StatusReader
|
||||
}
|
||||
-466
@@ -1,466 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube // import "helm.sh/helm/v4/pkg/kube"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
|
||||
deploymentutil "helm.sh/helm/v4/internal/third_party/k8s.io/kubernetes/deployment/util"
|
||||
)
|
||||
|
||||
// ReadyCheckerOption is a function that configures a ReadyChecker.
|
||||
type ReadyCheckerOption func(*ReadyChecker)
|
||||
|
||||
// PausedAsReady returns a ReadyCheckerOption that configures a ReadyChecker
|
||||
// to consider paused resources to be ready. For example a Deployment
|
||||
// with spec.paused equal to true would be considered ready.
|
||||
func PausedAsReady(pausedAsReady bool) ReadyCheckerOption {
|
||||
return func(c *ReadyChecker) {
|
||||
c.pausedAsReady = pausedAsReady
|
||||
}
|
||||
}
|
||||
|
||||
// CheckJobs returns a ReadyCheckerOption that configures a ReadyChecker
|
||||
// to consider readiness of Job resources.
|
||||
func CheckJobs(checkJobs bool) ReadyCheckerOption {
|
||||
return func(c *ReadyChecker) {
|
||||
c.checkJobs = checkJobs
|
||||
}
|
||||
}
|
||||
|
||||
// NewReadyChecker creates a new checker. Passed ReadyCheckerOptions can
|
||||
// be used to override defaults.
|
||||
func NewReadyChecker(cl kubernetes.Interface, opts ...ReadyCheckerOption) ReadyChecker {
|
||||
c := ReadyChecker{
|
||||
client: cl,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// ReadyChecker is a type that can check core Kubernetes types for readiness.
|
||||
type ReadyChecker struct {
|
||||
client kubernetes.Interface
|
||||
checkJobs bool
|
||||
pausedAsReady bool
|
||||
}
|
||||
|
||||
// IsReady checks if v is ready. It supports checking readiness for pods,
|
||||
// deployments, persistent volume claims, services, daemon sets, custom
|
||||
// resource definitions, stateful sets, replication controllers, jobs (optional),
|
||||
// and replica sets. All other resource kinds are always considered ready.
|
||||
//
|
||||
// IsReady will fetch the latest state of the object from the server prior to
|
||||
// performing readiness checks, and it will return any error encountered.
|
||||
func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, error) {
|
||||
switch value := AsVersioned(v).(type) {
|
||||
case *corev1.Pod:
|
||||
pod, err := c.client.CoreV1().Pods(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil || !c.isPodReady(pod) {
|
||||
return false, err
|
||||
}
|
||||
case *batchv1.Job:
|
||||
if c.checkJobs {
|
||||
job, err := c.client.BatchV1().Jobs(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
ready, err := c.jobReady(job)
|
||||
return ready, err
|
||||
}
|
||||
case *appsv1.Deployment:
|
||||
currentDeployment, err := c.client.AppsV1().Deployments(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// If paused deployment will never be ready
|
||||
if currentDeployment.Spec.Paused {
|
||||
return c.pausedAsReady, nil
|
||||
}
|
||||
// Find RS associated with deployment
|
||||
newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, c.client.AppsV1())
|
||||
if err != nil || newReplicaSet == nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.deploymentReady(newReplicaSet, currentDeployment) {
|
||||
return false, nil
|
||||
}
|
||||
case *corev1.PersistentVolumeClaim:
|
||||
claim, err := c.client.CoreV1().PersistentVolumeClaims(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.volumeReady(claim) {
|
||||
return false, nil
|
||||
}
|
||||
case *corev1.Service:
|
||||
svc, err := c.client.CoreV1().Services(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.serviceReady(svc) {
|
||||
return false, nil
|
||||
}
|
||||
case *appsv1.DaemonSet:
|
||||
ds, err := c.client.AppsV1().DaemonSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.daemonSetReady(ds) {
|
||||
return false, nil
|
||||
}
|
||||
case *apiextv1beta1.CustomResourceDefinition:
|
||||
if err := v.Get(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
crd := &apiextv1beta1.CustomResourceDefinition{}
|
||||
if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.crdBetaReady(*crd) {
|
||||
return false, nil
|
||||
}
|
||||
case *apiextv1.CustomResourceDefinition:
|
||||
if err := v.Get(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
crd := &apiextv1.CustomResourceDefinition{}
|
||||
if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.crdReady(*crd) {
|
||||
return false, nil
|
||||
}
|
||||
case *appsv1.StatefulSet:
|
||||
sts, err := c.client.AppsV1().StatefulSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.statefulSetReady(sts) {
|
||||
return false, nil
|
||||
}
|
||||
case *corev1.ReplicationController:
|
||||
rc, err := c.client.CoreV1().ReplicationControllers(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.replicationControllerReady(rc) {
|
||||
return false, nil
|
||||
}
|
||||
ready, err := c.podsReadyForObject(ctx, v.Namespace, value)
|
||||
if !ready || err != nil {
|
||||
return false, err
|
||||
}
|
||||
case *appsv1.ReplicaSet:
|
||||
rs, err := c.client.AppsV1().ReplicaSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !c.replicaSetReady(rs) {
|
||||
return false, nil
|
||||
}
|
||||
ready, err := c.podsReadyForObject(ctx, v.Namespace, value)
|
||||
if !ready || err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) podsReadyForObject(ctx context.Context, namespace string, obj runtime.Object) (bool, error) {
|
||||
pods, err := c.podsforObject(ctx, namespace, obj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, pod := range pods {
|
||||
if !c.isPodReady(&pod) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) podsforObject(ctx context.Context, namespace string, obj runtime.Object) ([]corev1.Pod, error) {
|
||||
selector, err := SelectorsForObject(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := getPods(ctx, c.client, namespace, selector.String())
|
||||
return list, err
|
||||
}
|
||||
|
||||
// isPodReady returns true if a pod is ready; false otherwise.
|
||||
func (c *ReadyChecker) isPodReady(pod *corev1.Pod) bool {
|
||||
for _, c := range pod.Status.Conditions {
|
||||
if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
slog.Debug("Pod is not ready", "namespace", pod.GetNamespace(), "name", pod.GetName())
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) jobReady(job *batchv1.Job) (bool, error) {
|
||||
if job.Status.Failed > *job.Spec.BackoffLimit {
|
||||
slog.Debug("Job is failed", "namespace", job.GetNamespace(), "name", job.GetName())
|
||||
// If a job is failed, it can't recover, so throw an error
|
||||
return false, fmt.Errorf("job is failed: %s/%s", job.GetNamespace(), job.GetName())
|
||||
}
|
||||
if job.Spec.Completions != nil && job.Status.Succeeded < *job.Spec.Completions {
|
||||
slog.Debug("Job is not completed", "namespace", job.GetNamespace(), "name", job.GetName())
|
||||
return false, nil
|
||||
}
|
||||
slog.Debug("Job is completed", "namespace", job.GetNamespace(), "name", job.GetName())
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) serviceReady(s *corev1.Service) bool {
|
||||
// ExternalName Services are external to cluster so helm shouldn't be checking to see if they're 'ready' (i.e. have an IP Set)
|
||||
if s.Spec.Type == corev1.ServiceTypeExternalName {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ensure that the service cluster IP is not empty
|
||||
if s.Spec.ClusterIP == "" {
|
||||
slog.Debug("Service does not have cluster IP address", "namespace", s.GetNamespace(), "name", s.GetName())
|
||||
return false
|
||||
}
|
||||
|
||||
// This checks if the service has a LoadBalancer and that balancer has an Ingress defined
|
||||
if s.Spec.Type == corev1.ServiceTypeLoadBalancer {
|
||||
// do not wait when at least 1 external IP is set
|
||||
if len(s.Spec.ExternalIPs) > 0 {
|
||||
slog.Debug("Service has external IP addresses", "namespace", s.GetNamespace(), "name", s.GetName(), "externalIPs", s.Spec.ExternalIPs)
|
||||
return true
|
||||
}
|
||||
|
||||
if s.Status.LoadBalancer.Ingress == nil {
|
||||
slog.Debug("Service does not have load balancer ingress IP address", "namespace", s.GetNamespace(), "name", s.GetName())
|
||||
return false
|
||||
}
|
||||
}
|
||||
slog.Debug("Service is ready", "namespace", s.GetNamespace(), "name", s.GetName(), "clusterIP", s.Spec.ClusterIP, "externalIPs", s.Spec.ExternalIPs)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) volumeReady(v *corev1.PersistentVolumeClaim) bool {
|
||||
if v.Status.Phase != corev1.ClaimBound {
|
||||
slog.Debug("PersistentVolumeClaim is not bound", "namespace", v.GetNamespace(), "name", v.GetName())
|
||||
return false
|
||||
}
|
||||
slog.Debug("PersistentVolumeClaim is bound", "namespace", v.GetNamespace(), "name", v.GetName(), "phase", v.Status.Phase)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deployment) bool {
|
||||
// Verify the replicaset readiness
|
||||
if !c.replicaSetReady(rs) {
|
||||
return false
|
||||
}
|
||||
// Verify the generation observed by the deployment controller matches the spec generation
|
||||
if dep.Status.ObservedGeneration != dep.Generation {
|
||||
slog.Debug("Deployment is not ready, observedGeneration does not match spec generation", "namespace", dep.GetNamespace(), "name", dep.GetName(), "actualGeneration", dep.Status.ObservedGeneration, "expectedGeneration", dep.Generation)
|
||||
return false
|
||||
}
|
||||
|
||||
expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep)
|
||||
if rs.Status.ReadyReplicas < expectedReady {
|
||||
slog.Debug("Deployment does not have enough pods ready", "namespace", dep.GetNamespace(), "name", dep.GetName(), "readyPods", rs.Status.ReadyReplicas, "totalPods", expectedReady)
|
||||
return false
|
||||
}
|
||||
slog.Debug("Deployment is ready", "namespace", dep.GetNamespace(), "name", dep.GetName(), "readyPods", rs.Status.ReadyReplicas, "totalPods", expectedReady)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool {
|
||||
// Verify the generation observed by the daemonSet controller matches the spec generation
|
||||
if ds.Status.ObservedGeneration != ds.Generation {
|
||||
slog.Debug("DaemonSet is not ready, observedGeneration does not match spec generation", "namespace", ds.GetNamespace(), "name", ds.GetName(), "observedGeneration", ds.Status.ObservedGeneration, "expectedGeneration", ds.Generation)
|
||||
return false
|
||||
}
|
||||
|
||||
// If the update strategy is not a rolling update, there will be nothing to wait for
|
||||
if ds.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType {
|
||||
return true
|
||||
}
|
||||
|
||||
// Make sure all the updated pods have been scheduled
|
||||
if ds.Status.UpdatedNumberScheduled != ds.Status.DesiredNumberScheduled {
|
||||
slog.Debug("DaemonSet does not have enough Pods scheduled", "namespace", ds.GetNamespace(), "name", ds.GetName(), "scheduledPods", ds.Status.UpdatedNumberScheduled, "totalPods", ds.Status.DesiredNumberScheduled)
|
||||
return false
|
||||
}
|
||||
maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable, int(ds.Status.DesiredNumberScheduled), true)
|
||||
if err != nil {
|
||||
// If for some reason the value is invalid, set max unavailable to the
|
||||
// number of desired replicas. This is the same behavior as the
|
||||
// `MaxUnavailable` function in deploymentutil
|
||||
maxUnavailable = int(ds.Status.DesiredNumberScheduled)
|
||||
}
|
||||
|
||||
expectedReady := int(ds.Status.DesiredNumberScheduled) - maxUnavailable
|
||||
if int(ds.Status.NumberReady) < expectedReady {
|
||||
slog.Debug("DaemonSet does not have enough Pods ready", "namespace", ds.GetNamespace(), "name", ds.GetName(), "readyPods", ds.Status.NumberReady, "totalPods", expectedReady)
|
||||
return false
|
||||
}
|
||||
slog.Debug("DaemonSet is ready", "namespace", ds.GetNamespace(), "name", ds.GetName(), "readyPods", ds.Status.NumberReady, "totalPods", expectedReady)
|
||||
return true
|
||||
}
|
||||
|
||||
// Because the v1 extensions API is not available on all supported k8s versions
|
||||
// yet and because Go doesn't support generics, we need to have a duplicate
|
||||
// function to support the v1beta1 types
|
||||
func (c *ReadyChecker) crdBetaReady(crd apiextv1beta1.CustomResourceDefinition) bool {
|
||||
for _, cond := range crd.Status.Conditions {
|
||||
switch cond.Type {
|
||||
case apiextv1beta1.Established:
|
||||
if cond.Status == apiextv1beta1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
case apiextv1beta1.NamesAccepted:
|
||||
if cond.Status == apiextv1beta1.ConditionFalse {
|
||||
// This indicates a naming conflict, but it's probably not the
|
||||
// job of this function to fail because of that. Instead,
|
||||
// we treat it as a success, since the process should be able to
|
||||
// continue.
|
||||
return true
|
||||
}
|
||||
default:
|
||||
// intentionally left empty
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool {
|
||||
for _, cond := range crd.Status.Conditions {
|
||||
switch cond.Type {
|
||||
case apiextv1.Established:
|
||||
if cond.Status == apiextv1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
case apiextv1.NamesAccepted:
|
||||
if cond.Status == apiextv1.ConditionFalse {
|
||||
// This indicates a naming conflict, but it's probably not the
|
||||
// job of this function to fail because of that. Instead,
|
||||
// we treat it as a success, since the process should be able to
|
||||
// continue.
|
||||
return true
|
||||
}
|
||||
default:
|
||||
// intentionally left empty
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
|
||||
// Verify the generation observed by the statefulSet controller matches the spec generation
|
||||
if sts.Status.ObservedGeneration != sts.Generation {
|
||||
slog.Debug("StatefulSet is not ready, observedGeneration doest not match spec generation", "namespace", sts.GetNamespace(), "name", sts.GetName(), "actualGeneration", sts.Status.ObservedGeneration, "expectedGeneration", sts.Generation)
|
||||
return false
|
||||
}
|
||||
|
||||
// If the update strategy is not a rolling update, there will be nothing to wait for
|
||||
if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType {
|
||||
slog.Debug("StatefulSet skipped ready check", "namespace", sts.GetNamespace(), "name", sts.GetName(), "updateStrategy", sts.Spec.UpdateStrategy.Type)
|
||||
return true
|
||||
}
|
||||
|
||||
// Dereference all the pointers because StatefulSets like them
|
||||
var partition int
|
||||
// 1 is the default for replicas if not set
|
||||
replicas := 1
|
||||
// For some reason, even if the update strategy is a rolling update, the
|
||||
// actual rollingUpdate field can be nil. If it is, we can safely assume
|
||||
// there is no partition value
|
||||
if sts.Spec.UpdateStrategy.RollingUpdate != nil && sts.Spec.UpdateStrategy.RollingUpdate.Partition != nil {
|
||||
partition = int(*sts.Spec.UpdateStrategy.RollingUpdate.Partition)
|
||||
}
|
||||
if sts.Spec.Replicas != nil {
|
||||
replicas = int(*sts.Spec.Replicas)
|
||||
}
|
||||
|
||||
// Because an update strategy can use partitioning, we need to calculate the
|
||||
// number of updated replicas we should have. For example, if the replicas
|
||||
// is set to 3 and the partition is 2, we'd expect only one pod to be
|
||||
// updated
|
||||
expectedReplicas := replicas - partition
|
||||
|
||||
// Make sure all the updated pods have been scheduled
|
||||
if int(sts.Status.UpdatedReplicas) < expectedReplicas {
|
||||
slog.Debug("StatefulSet does not have enough Pods scheduled", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.UpdatedReplicas, "totalPods", expectedReplicas)
|
||||
return false
|
||||
}
|
||||
|
||||
if int(sts.Status.ReadyReplicas) != replicas {
|
||||
slog.Debug("StatefulSet does not have enough Pods ready", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.ReadyReplicas, "totalPods", replicas)
|
||||
return false
|
||||
}
|
||||
// This check only makes sense when all partitions are being upgraded otherwise during a
|
||||
// partitioned rolling upgrade, this condition will never evaluate to true, leading to
|
||||
// error.
|
||||
if partition == 0 && sts.Status.CurrentRevision != sts.Status.UpdateRevision {
|
||||
slog.Debug("StatefulSet is not ready, currentRevision does not match updateRevision", "namespace", sts.GetNamespace(), "name", sts.GetName(), "currentRevision", sts.Status.CurrentRevision, "updateRevision", sts.Status.UpdateRevision)
|
||||
return false
|
||||
}
|
||||
slog.Debug("StatefulSet is ready", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.ReadyReplicas, "totalPods", replicas)
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationController) bool {
|
||||
// Verify the generation observed by the replicationController controller matches the spec generation
|
||||
if rc.Status.ObservedGeneration != rc.Generation {
|
||||
slog.Debug("ReplicationController is not ready, observedGeneration doest not match spec generation", "namespace", rc.GetNamespace(), "name", rc.GetName(), "actualGeneration", rc.Status.ObservedGeneration, "expectedGeneration", rc.Generation)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ReadyChecker) replicaSetReady(rs *appsv1.ReplicaSet) bool {
|
||||
// Verify the generation observed by the replicaSet controller matches the spec generation
|
||||
if rs.Status.ObservedGeneration != rs.Generation {
|
||||
slog.Debug("ReplicaSet is not ready, observedGeneration doest not match spec generation", "namespace", rs.GetNamespace(), "name", rs.GetName(), "actualGeneration", rs.Status.ObservedGeneration, "expectedGeneration", rs.Generation)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getPods(ctx context.Context, client kubernetes.Interface, namespace, selector string) ([]corev1.Pod, error) {
|
||||
list, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
|
||||
LabelSelector: selector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list pods: %w", err)
|
||||
}
|
||||
return list.Items, nil
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube // import "helm.sh/helm/v4/pkg/kube"
|
||||
|
||||
import "k8s.io/cli-runtime/pkg/resource"
|
||||
|
||||
// ResourceList provides convenience methods for comparing collections of Infos.
|
||||
type ResourceList []*resource.Info
|
||||
|
||||
// Append adds an Info to the Result.
|
||||
func (r *ResourceList) Append(val *resource.Info) {
|
||||
*r = append(*r, val)
|
||||
}
|
||||
|
||||
// Visit implements resource.Visitor. The visitor stops if fn returns an error.
|
||||
func (r ResourceList) Visit(fn resource.VisitorFunc) error {
|
||||
for _, i := range r {
|
||||
if err := fn(i, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter returns a new Result with Infos that satisfy the predicate fn.
|
||||
func (r ResourceList) Filter(fn func(*resource.Info) bool) ResourceList {
|
||||
var result ResourceList
|
||||
for _, i := range r {
|
||||
if fn(i) {
|
||||
result.Append(i)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Get returns the Info from the result that matches the name and kind.
|
||||
func (r ResourceList) Get(info *resource.Info) *resource.Info {
|
||||
for _, i := range r {
|
||||
if isMatchingInfo(i, info) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Contains checks to see if an object exists.
|
||||
func (r ResourceList) Contains(info *resource.Info) bool {
|
||||
for _, i := range r {
|
||||
if isMatchingInfo(i, info) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Difference will return a new Result with objects not contained in rs.
|
||||
func (r ResourceList) Difference(rs ResourceList) ResourceList {
|
||||
return r.Filter(func(info *resource.Info) bool {
|
||||
return !rs.Contains(info)
|
||||
})
|
||||
}
|
||||
|
||||
// Intersect will return a new Result with objects contained in both Results.
|
||||
func (r ResourceList) Intersect(rs ResourceList) ResourceList {
|
||||
return r.Filter(rs.Contains)
|
||||
}
|
||||
|
||||
// isMatchingInfo returns true if infos match on Name, Namespace, Group and Kind.
|
||||
//
|
||||
// IMPORTANT: Version is intentionally excluded from the comparison. Resources
|
||||
// served by the same CRD at different API versions (e.g. v2beta1 vs v2beta2)
|
||||
// share the same underlying storage in the Kubernetes API server. Comparing
|
||||
// the full GroupVersionKind causes Difference() to treat a version change as
|
||||
// a resource removal + addition, which makes Helm delete the resource it just
|
||||
// created during upgrades. See https://github.com/helm/helm/issues/31768
|
||||
func isMatchingInfo(a, b *resource.Info) bool {
|
||||
return a.Name == b.Name && a.Namespace == b.Namespace && a.Mapping.GroupVersionKind.GroupKind() == b.Mapping.GroupVersionKind.GroupKind()
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube // import "helm.sh/helm/v4/pkg/kube"
|
||||
|
||||
// ResourcePolicyAnno is the annotation name for a resource policy
|
||||
const ResourcePolicyAnno = "helm.sh/resource-policy"
|
||||
|
||||
// KeepPolicy is the resource policy type for keep
|
||||
//
|
||||
// This resource policy type allows resources to skip being deleted
|
||||
//
|
||||
// during an uninstallRelease action.
|
||||
const KeepPolicy = "keep"
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube
|
||||
|
||||
// Result contains the information of created, updated, and deleted resources
|
||||
// for various kube API calls along with helper methods for using those
|
||||
// resources
|
||||
type Result struct {
|
||||
Created ResourceList
|
||||
Updated ResourceList
|
||||
Deleted ResourceList
|
||||
}
|
||||
|
||||
// If needed, we can add methods to the Result type for things like diffing
|
||||
-292
@@ -1,292 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube // import "helm.sh/helm/v4/pkg/kube"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/aggregator"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
|
||||
"github.com/fluxcd/cli-utils/pkg/kstatus/watcher"
|
||||
"github.com/fluxcd/cli-utils/pkg/object"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/dynamic"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
|
||||
"helm.sh/helm/v4/internal/logging"
|
||||
helmStatusReaders "helm.sh/helm/v4/internal/statusreaders"
|
||||
)
|
||||
|
||||
type statusWaiter struct {
|
||||
client dynamic.Interface
|
||||
restMapper meta.RESTMapper
|
||||
ctx context.Context
|
||||
watchUntilReadyCtx context.Context
|
||||
waitCtx context.Context
|
||||
waitWithJobsCtx context.Context
|
||||
waitForDeleteCtx context.Context
|
||||
readers []engine.StatusReader
|
||||
logging.LogHolder
|
||||
}
|
||||
|
||||
// DefaultStatusWatcherTimeout is the timeout used by the status waiter when a
|
||||
// zero timeout is provided. This prevents callers from accidentally passing a
|
||||
// zero value (which would immediately cancel the context) and getting
|
||||
// "context deadline exceeded" errors. SDK callers can rely on this default
|
||||
// when they don't set a timeout.
|
||||
var DefaultStatusWatcherTimeout = 30 * time.Second
|
||||
|
||||
func alwaysReady(_ *unstructured.Unstructured) (*status.Result, error) {
|
||||
return &status.Result{
|
||||
Status: status.CurrentStatus,
|
||||
Message: "Resource is current",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *statusWaiter) WatchUntilReady(resourceList ResourceList, timeout time.Duration) error {
|
||||
if timeout == 0 {
|
||||
timeout = DefaultStatusWatcherTimeout
|
||||
}
|
||||
ctx, cancel := w.contextWithTimeout(w.watchUntilReadyCtx, timeout)
|
||||
defer cancel()
|
||||
w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout)
|
||||
sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper)
|
||||
jobSR := helmStatusReaders.NewCustomJobStatusReader(w.restMapper)
|
||||
podSR := helmStatusReaders.NewCustomPodStatusReader(w.restMapper)
|
||||
// We don't want to wait on any other resources as watchUntilReady is only for Helm hooks.
|
||||
// If custom readers are defined they can be used as Helm hooks support any resource.
|
||||
// We put them in front since the DelegatingStatusReader uses the first reader that matches.
|
||||
genericSR := statusreaders.NewGenericStatusReader(w.restMapper, alwaysReady)
|
||||
|
||||
sr := &statusreaders.DelegatingStatusReader{
|
||||
StatusReaders: append(w.readers, jobSR, podSR, genericSR),
|
||||
}
|
||||
sw.StatusReader = sr
|
||||
return w.wait(ctx, resourceList, sw)
|
||||
}
|
||||
|
||||
func (w *statusWaiter) Wait(resourceList ResourceList, timeout time.Duration) error {
|
||||
if timeout == 0 {
|
||||
timeout = DefaultStatusWatcherTimeout
|
||||
}
|
||||
ctx, cancel := w.contextWithTimeout(w.waitCtx, timeout)
|
||||
defer cancel()
|
||||
w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout)
|
||||
sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper)
|
||||
sw.StatusReader = statusreaders.NewStatusReader(w.restMapper, w.readers...)
|
||||
return w.wait(ctx, resourceList, sw)
|
||||
}
|
||||
|
||||
func (w *statusWaiter) WaitWithJobs(resourceList ResourceList, timeout time.Duration) error {
|
||||
if timeout == 0 {
|
||||
timeout = DefaultStatusWatcherTimeout
|
||||
}
|
||||
ctx, cancel := w.contextWithTimeout(w.waitWithJobsCtx, timeout)
|
||||
defer cancel()
|
||||
w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout)
|
||||
sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper)
|
||||
newCustomJobStatusReader := helmStatusReaders.NewCustomJobStatusReader(w.restMapper)
|
||||
readers := append([]engine.StatusReader(nil), w.readers...)
|
||||
readers = append(readers, newCustomJobStatusReader)
|
||||
customSR := statusreaders.NewStatusReader(w.restMapper, readers...)
|
||||
sw.StatusReader = customSR
|
||||
return w.wait(ctx, resourceList, sw)
|
||||
}
|
||||
|
||||
func (w *statusWaiter) WaitForDelete(resourceList ResourceList, timeout time.Duration) error {
|
||||
if timeout == 0 {
|
||||
timeout = DefaultStatusWatcherTimeout
|
||||
}
|
||||
ctx, cancel := w.contextWithTimeout(w.waitForDeleteCtx, timeout)
|
||||
defer cancel()
|
||||
w.Logger().Debug("waiting for resources to be deleted", "count", len(resourceList), "timeout", timeout)
|
||||
sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper)
|
||||
return w.waitForDelete(ctx, resourceList, sw)
|
||||
}
|
||||
|
||||
func (w *statusWaiter) waitForDelete(ctx context.Context, resourceList ResourceList, sw watcher.StatusWatcher) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
resources := []object.ObjMetadata{}
|
||||
for _, resource := range resourceList {
|
||||
obj, err := object.RuntimeToObjMeta(resource.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources = append(resources, obj)
|
||||
}
|
||||
eventCh := sw.Watch(cancelCtx, resources, watcher.Options{
|
||||
RESTScopeStrategy: watcher.RESTScopeNamespace,
|
||||
})
|
||||
statusCollector := collector.NewResourceStatusCollector(resources)
|
||||
done := statusCollector.ListenWithObserver(eventCh, statusObserver(cancel, status.NotFoundStatus, w.Logger()))
|
||||
<-done
|
||||
|
||||
if statusCollector.Error != nil {
|
||||
return statusCollector.Error
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
for _, id := range resources {
|
||||
rs := statusCollector.ResourceStatuses[id]
|
||||
if rs.Status == status.NotFoundStatus || rs.Status == status.UnknownStatus {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, fmt.Errorf("resource %s/%s/%s still exists. status: %s, message: %s",
|
||||
rs.Identifier.GroupKind.Kind, rs.Identifier.Namespace, rs.Identifier.Name, rs.Status, rs.Message))
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *statusWaiter) wait(ctx context.Context, resourceList ResourceList, sw watcher.StatusWatcher) error {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
resources := []object.ObjMetadata{}
|
||||
for _, resource := range resourceList {
|
||||
switch value := AsVersioned(resource).(type) {
|
||||
case *appsv1.Deployment:
|
||||
if value.Spec.Paused {
|
||||
continue
|
||||
}
|
||||
}
|
||||
obj, err := object.RuntimeToObjMeta(resource.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources = append(resources, obj)
|
||||
}
|
||||
|
||||
eventCh := sw.Watch(cancelCtx, resources, watcher.Options{
|
||||
RESTScopeStrategy: watcher.RESTScopeNamespace,
|
||||
})
|
||||
statusCollector := collector.NewResourceStatusCollector(resources)
|
||||
done := statusCollector.ListenWithObserver(eventCh, statusObserver(cancel, status.CurrentStatus, w.Logger()))
|
||||
<-done
|
||||
|
||||
if statusCollector.Error != nil {
|
||||
return statusCollector.Error
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
for _, id := range resources {
|
||||
rs := statusCollector.ResourceStatuses[id]
|
||||
if rs.Status == status.CurrentStatus {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, fmt.Errorf("resource %s/%s/%s not ready. status: %s, message: %s",
|
||||
rs.Identifier.GroupKind.Kind, rs.Identifier.Namespace, rs.Identifier.Name, rs.Status, rs.Message))
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *statusWaiter) contextWithTimeout(methodCtx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
if methodCtx == nil {
|
||||
methodCtx = w.ctx
|
||||
}
|
||||
return contextWithTimeout(methodCtx, timeout)
|
||||
}
|
||||
|
||||
func contextWithTimeout(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return watchtools.ContextWithOptionalTimeout(ctx, timeout)
|
||||
}
|
||||
|
||||
func statusObserver(cancel context.CancelFunc, desired status.Status, logger *slog.Logger) collector.ObserverFunc {
|
||||
return func(statusCollector *collector.ResourceStatusCollector, _ event.Event) {
|
||||
var rss []*event.ResourceStatus
|
||||
var nonDesiredResources []*event.ResourceStatus
|
||||
for _, rs := range statusCollector.ResourceStatuses {
|
||||
if rs == nil {
|
||||
continue
|
||||
}
|
||||
// If a resource is already deleted before waiting has started, it will show as unknown.
|
||||
// This check ensures we don't wait forever for a resource that is already deleted.
|
||||
if rs.Status == status.UnknownStatus && desired == status.NotFoundStatus {
|
||||
continue
|
||||
}
|
||||
// Failed is a terminal state. This check ensures we don't wait forever for a resource
|
||||
// that has already failed, as intervention is required to resolve the failure.
|
||||
if rs.Status == status.FailedStatus && desired == status.CurrentStatus {
|
||||
continue
|
||||
}
|
||||
rss = append(rss, rs)
|
||||
if rs.Status != desired {
|
||||
nonDesiredResources = append(nonDesiredResources, rs)
|
||||
}
|
||||
}
|
||||
|
||||
if aggregator.AggregateStatus(rss, desired) == desired {
|
||||
logger.Debug("all resources achieved desired status", "desiredStatus", desired, "resourceCount", len(rss))
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
if len(nonDesiredResources) > 0 {
|
||||
// Log a single resource so the user knows what they're waiting for without an overwhelming amount of output
|
||||
sort.Slice(nonDesiredResources, func(i, j int) bool {
|
||||
return nonDesiredResources[i].Identifier.Name < nonDesiredResources[j].Identifier.Name
|
||||
})
|
||||
first := nonDesiredResources[0]
|
||||
logger.Debug("waiting for resource", "namespace", first.Identifier.Namespace, "name", first.Identifier.Name, "kind", first.Identifier.GroupKind.Kind, "expectedStatus", desired, "actualStatus", first.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type hookOnlyWaiter struct {
|
||||
sw *statusWaiter
|
||||
}
|
||||
|
||||
func (w *hookOnlyWaiter) WatchUntilReady(resourceList ResourceList, timeout time.Duration) error {
|
||||
return w.sw.WatchUntilReady(resourceList, timeout)
|
||||
}
|
||||
|
||||
func (w *hookOnlyWaiter) Wait(_ ResourceList, _ time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *hookOnlyWaiter) WaitWithJobs(_ ResourceList, _ time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *hookOnlyWaiter) WaitForDelete(_ ResourceList, _ time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
-345
@@ -1,345 +0,0 @@
|
||||
/*
|
||||
Copyright The Helm 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 kube // import "helm.sh/helm/v4/pkg/kube"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
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/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
cachetools "k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
)
|
||||
|
||||
// legacyWaiter is the legacy implementation of the Waiter interface. This logic was used by default in Helm 3
|
||||
// Helm 4 now uses the StatusWaiter implementation instead
|
||||
type legacyWaiter struct {
|
||||
c ReadyChecker
|
||||
kubeClient *kubernetes.Clientset
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (hw *legacyWaiter) Wait(resources ResourceList, timeout time.Duration) error {
|
||||
hw.c = NewReadyChecker(hw.kubeClient, PausedAsReady(true))
|
||||
return hw.waitForResources(resources, timeout)
|
||||
}
|
||||
|
||||
func (hw *legacyWaiter) WaitWithJobs(resources ResourceList, timeout time.Duration) error {
|
||||
hw.c = NewReadyChecker(hw.kubeClient, PausedAsReady(true), CheckJobs(true))
|
||||
return hw.waitForResources(resources, timeout)
|
||||
}
|
||||
|
||||
// waitForResources polls to get the current status of all pods, PVCs, Services and
|
||||
// Jobs(optional) until all are ready or a timeout is reached
|
||||
func (hw *legacyWaiter) waitForResources(created ResourceList, timeout time.Duration) error {
|
||||
slog.Debug("beginning wait for resources", "count", len(created), "timeout", timeout)
|
||||
|
||||
ctx, cancel := hw.contextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
|
||||
numberOfErrors := make([]int, len(created))
|
||||
for i := range numberOfErrors {
|
||||
numberOfErrors[i] = 0
|
||||
}
|
||||
|
||||
return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) {
|
||||
waitRetries := 30
|
||||
for i, v := range created {
|
||||
ready, err := hw.c.IsReady(ctx, v)
|
||||
|
||||
if waitRetries > 0 && hw.isRetryableError(err, v) {
|
||||
numberOfErrors[i]++
|
||||
if numberOfErrors[i] > waitRetries {
|
||||
slog.Debug("max number of retries reached", "resource", v.Name, "retries", numberOfErrors[i])
|
||||
return false, err
|
||||
}
|
||||
slog.Debug("retrying resource readiness", "resource", v.Name, "currentRetries", numberOfErrors[i]-1, "maxRetries", waitRetries)
|
||||
return false, nil
|
||||
}
|
||||
numberOfErrors[i] = 0
|
||||
if !ready {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (hw *legacyWaiter) isRetryableError(err error, resource *resource.Info) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
slog.Debug(
|
||||
"error received when checking resource status",
|
||||
slog.String("resource", resource.Name),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
if ev, ok := err.(*apierrors.StatusError); ok {
|
||||
statusCode := ev.Status().Code
|
||||
retryable := hw.isRetryableHTTPStatusCode(statusCode)
|
||||
slog.Debug(
|
||||
"status code received",
|
||||
slog.String("resource", resource.Name),
|
||||
slog.Int("statusCode", int(statusCode)),
|
||||
slog.Bool("retryable", retryable),
|
||||
)
|
||||
return retryable
|
||||
}
|
||||
slog.Debug("retryable error assumed", "resource", resource.Name)
|
||||
return true
|
||||
}
|
||||
|
||||
func (hw *legacyWaiter) isRetryableHTTPStatusCode(httpStatusCode int32) bool {
|
||||
return httpStatusCode == 0 || httpStatusCode == http.StatusTooManyRequests || (httpStatusCode >= 500 && httpStatusCode != http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// WaitForDelete polls to check if all the resources are deleted or a timeout is reached
|
||||
func (hw *legacyWaiter) WaitForDelete(deleted ResourceList, timeout time.Duration) error {
|
||||
slog.Debug("beginning wait for resources to be deleted", "count", len(deleted), "timeout", timeout)
|
||||
|
||||
startTime := time.Now()
|
||||
ctx, cancel := hw.contextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
|
||||
err := wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(_ context.Context) (bool, error) {
|
||||
for _, v := range deleted {
|
||||
err := v.Get()
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
elapsed := time.Since(startTime).Round(time.Second)
|
||||
if err != nil {
|
||||
slog.Debug("wait for resources failed", slog.Duration("elapsed", elapsed), slog.Any("error", err))
|
||||
} else {
|
||||
slog.Debug("wait for resources succeeded", slog.Duration("elapsed", elapsed))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// SelectorsForObject returns the pod label selector for a given object
|
||||
//
|
||||
// Modified version of https://github.com/kubernetes/kubernetes/blob/v1.14.1/pkg/kubectl/polymorphichelpers/helpers.go#L84
|
||||
func SelectorsForObject(object runtime.Object) (selector labels.Selector, err error) {
|
||||
switch t := object.(type) {
|
||||
case *extensionsv1beta1.ReplicaSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1.ReplicaSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1beta2.ReplicaSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *corev1.ReplicationController:
|
||||
selector = labels.SelectorFromSet(t.Spec.Selector)
|
||||
case *appsv1.StatefulSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1beta1.StatefulSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1beta2.StatefulSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *extensionsv1beta1.DaemonSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1.DaemonSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1beta2.DaemonSet:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *extensionsv1beta1.Deployment:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1.Deployment:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1beta1.Deployment:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *appsv1beta2.Deployment:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *batchv1.Job:
|
||||
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
|
||||
case *corev1.Service:
|
||||
if len(t.Spec.Selector) == 0 {
|
||||
return nil, fmt.Errorf("invalid service '%s': Service is defined without a selector", t.Name)
|
||||
}
|
||||
selector = labels.SelectorFromSet(t.Spec.Selector)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("selector for %T not implemented", object)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return selector, fmt.Errorf("invalid label selector: %w", err)
|
||||
}
|
||||
|
||||
return selector, nil
|
||||
}
|
||||
|
||||
func (hw *legacyWaiter) watchTimeout(t time.Duration) func(*resource.Info) error {
|
||||
return func(info *resource.Info) error {
|
||||
return hw.watchUntilReady(t, info)
|
||||
}
|
||||
}
|
||||
|
||||
// WatchUntilReady watches the resources given and waits until it is ready.
|
||||
//
|
||||
// This method is mainly for hook implementations. It watches for a resource to
|
||||
// hit a particular milestone. The milestone depends on the Kind.
|
||||
//
|
||||
// For most kinds, it checks to see if the resource is marked as Added or Modified
|
||||
// by the Kubernetes event stream. For some kinds, it does more:
|
||||
//
|
||||
// - Jobs: A job is marked "Ready" when it has successfully completed. This is
|
||||
// ascertained by watching the Status fields in a job's output.
|
||||
// - Pods: A pod is marked "Ready" when it has successfully completed. This is
|
||||
// ascertained by watching the status.phase field in a pod's output.
|
||||
//
|
||||
// Handling for other kinds will be added as necessary.
|
||||
func (hw *legacyWaiter) WatchUntilReady(resources ResourceList, timeout time.Duration) error {
|
||||
// For jobs, there's also the option to do poll c.Jobs(namespace).Get():
|
||||
// https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300
|
||||
return perform(resources, hw.watchTimeout(timeout))
|
||||
}
|
||||
|
||||
func (hw *legacyWaiter) watchUntilReady(timeout time.Duration, info *resource.Info) error {
|
||||
kind := info.Mapping.GroupVersionKind.Kind
|
||||
switch kind {
|
||||
case "Job", "Pod":
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
slog.Debug("watching for resource changes", "kind", kind, "resource", info.Name, "timeout", timeout)
|
||||
|
||||
// Use a selector on the name of the resource. This should be unique for the
|
||||
// given version and kind
|
||||
selector, err := fields.ParseSelector(fmt.Sprintf("metadata.name=%s", info.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lw := cachetools.NewListWatchFromClient(info.Client, info.Mapping.Resource.Resource, info.Namespace, selector)
|
||||
|
||||
// What we watch for depends on the Kind.
|
||||
// - For a Job, we watch for completion.
|
||||
// - For all else, we watch until Ready.
|
||||
// In the future, we might want to add some special logic for types
|
||||
// like Ingress, Volume, etc.
|
||||
|
||||
ctx, cancel := hw.contextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
_, err = watchtools.UntilWithSync(ctx, lw, &unstructured.Unstructured{}, nil, func(e watch.Event) (bool, error) {
|
||||
// Make sure the incoming object is versioned as we use unstructured
|
||||
// objects when we build manifests
|
||||
obj := convertWithMapper(e.Object, info.Mapping)
|
||||
switch e.Type {
|
||||
case watch.Added, watch.Modified:
|
||||
// For things like a secret or a config map, this is the best indicator
|
||||
// we get. We care mostly about jobs, where what we want to see is
|
||||
// the status go into a good state. For other types, like ReplicaSet
|
||||
// we don't really do anything to support these as hooks.
|
||||
slog.Debug("add/modify event received", "resource", info.Name, "eventType", e.Type)
|
||||
|
||||
switch kind {
|
||||
case "Job":
|
||||
return hw.waitForJob(obj, info.Name)
|
||||
case "Pod":
|
||||
return hw.waitForPodSuccess(obj, info.Name)
|
||||
}
|
||||
return true, nil
|
||||
case watch.Deleted:
|
||||
slog.Debug("deleted event received", "resource", info.Name)
|
||||
return true, nil
|
||||
case watch.Error:
|
||||
// Handle error and return with an error.
|
||||
slog.Error("error event received", "resource", info.Name)
|
||||
return true, fmt.Errorf("failed to deploy %s", info.Name)
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// waitForJob is a helper that waits for a job to complete.
|
||||
//
|
||||
// This operates on an event returned from a watcher.
|
||||
func (hw *legacyWaiter) waitForJob(obj runtime.Object, name string) (bool, error) {
|
||||
o, ok := obj.(*batchv1.Job)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("expected %s to be a *batch.Job, got %T", name, obj)
|
||||
}
|
||||
|
||||
for _, c := range o.Status.Conditions {
|
||||
if c.Type == batchv1.JobComplete && c.Status == "True" {
|
||||
return true, nil
|
||||
} else if c.Type == batchv1.JobFailed && c.Status == "True" {
|
||||
slog.Error("job failed", "job", name, "reason", c.Reason)
|
||||
return true, fmt.Errorf("job %s failed: %s", name, c.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
slog.Debug("job status update", "job", name, "active", o.Status.Active, "failed", o.Status.Failed, "succeeded", o.Status.Succeeded)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// waitForPodSuccess is a helper that waits for a pod to complete.
|
||||
//
|
||||
// This operates on an event returned from a watcher.
|
||||
func (hw *legacyWaiter) waitForPodSuccess(obj runtime.Object, name string) (bool, error) {
|
||||
o, ok := obj.(*corev1.Pod)
|
||||
if !ok {
|
||||
return true, fmt.Errorf("expected %s to be a *v1.Pod, got %T", name, obj)
|
||||
}
|
||||
|
||||
switch o.Status.Phase {
|
||||
case corev1.PodSucceeded:
|
||||
slog.Debug("pod succeeded", "pod", o.Name)
|
||||
return true, nil
|
||||
case corev1.PodFailed:
|
||||
slog.Error("pod failed", "pod", o.Name)
|
||||
return true, fmt.Errorf("pod %s failed", o.Name)
|
||||
case corev1.PodPending:
|
||||
slog.Debug("pod pending", "pod", o.Name)
|
||||
case corev1.PodRunning:
|
||||
slog.Debug("pod running", "pod", o.Name)
|
||||
case corev1.PodUnknown:
|
||||
slog.Debug("pod unknown", "pod", o.Name)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (hw *legacyWaiter) contextWithTimeout(timeout time.Duration) (context.Context, context.CancelFunc) {
|
||||
return contextWithTimeout(hw.ctx, timeout)
|
||||
}
|
||||
vendor/helm.sh/helm/v4/pkg/kube/roundtripper.go → vendor/helm.sh/helm/v4/pkg/kubeenv/roundtripper.go
Vendored
+5
-1
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kube
|
||||
// Package kubeenv holds small, cycle-free Kubernetes client helpers shared by
|
||||
// higher-level packages (for example pkg/cli and pkg/kube).
|
||||
package kubeenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -24,6 +26,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RetryingRoundTripper retries transient Kubernetes API server errors on a
|
||||
// wrapped [http.RoundTripper].
|
||||
type RetryingRoundTripper struct {
|
||||
Wrapped http.RoundTripper
|
||||
}
|
||||
+4
-4
@@ -25,9 +25,9 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp" //nolint
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet" //nolint
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
@@ -332,7 +332,7 @@ func parseMessageBlock(data []byte) (*SumCollection, error) {
|
||||
//
|
||||
// This is the generic version that can work with any metadata type.
|
||||
// The metadata parameter should be a pointer to a struct that can be unmarshaled from YAML.
|
||||
func ParseMessageBlock(data []byte, metadata interface{}, sums *SumCollection) error {
|
||||
func ParseMessageBlock(data []byte, metadata any, sums *SumCollection) error {
|
||||
parts := bytes.Split(data, []byte("\n...\n"))
|
||||
if len(parts) < 2 {
|
||||
return errors.New("message block must have at least two parts")
|
||||
|
||||
+7
-4
@@ -202,13 +202,15 @@ func ClientOptCredentialsFile(credentialsFile string) ClientOption {
|
||||
}
|
||||
}
|
||||
|
||||
// ClientOptHTTPClient returns a function that sets the httpClient setting on a client options set
|
||||
// ClientOptHTTPClient returns a function that sets the HTTP client for the registry client.
|
||||
func ClientOptHTTPClient(httpClient *http.Client) ClientOption {
|
||||
return func(client *Client) {
|
||||
client.httpClient = httpClient
|
||||
}
|
||||
}
|
||||
|
||||
// ClientOptPlainHTTP returns a function that enables plain HTTP (non-TLS)
|
||||
// communication for the registry client.
|
||||
func ClientOptPlainHTTP() ClientOption {
|
||||
return func(c *Client) {
|
||||
c.plainHTTP = true
|
||||
@@ -236,7 +238,7 @@ func warnIfHostHasPath(host string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Login logs into a registry
|
||||
// Login authenticates the client with a remote OCI registry using the provided host and options.
|
||||
func (c *Client) Login(host string, options ...LoginOption) error {
|
||||
for _, option := range options {
|
||||
option(&loginOperation{host, c})
|
||||
@@ -282,7 +284,8 @@ func LoginOptBasicAuth(username string, password string) LoginOption {
|
||||
}
|
||||
}
|
||||
|
||||
// LoginOptPlainText returns a function that allows plaintext (HTTP) login
|
||||
// LoginOptPlainText returns a function that enables plaintext (HTTP) login
|
||||
// instead of HTTPS for the registry client.
|
||||
func LoginOptPlainText(isPlainText bool) LoginOption {
|
||||
return func(o *loginOperation) {
|
||||
o.client.plainHTTP = isPlainText
|
||||
@@ -882,7 +885,7 @@ func (c *Client) ValidateReference(ref, version string, u *url.URL) (string, *ur
|
||||
tag = version
|
||||
} else {
|
||||
// Retrieve list of repository tags
|
||||
tags, err := c.Tags(strings.TrimPrefix(ref, fmt.Sprintf("%s://", OCIScheme)))
|
||||
tags, err := c.Tags(strings.TrimPrefix(ref, OCIScheme+"://"))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ package registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -190,7 +191,7 @@ func GetPluginName(source string) (string, error) {
|
||||
// e.g., "ghcr.io/user/plugin-name:v1.0.0" -> Repository: "user/plugin-name"
|
||||
repository := ref.Repository
|
||||
if repository == "" {
|
||||
return "", fmt.Errorf("invalid OCI reference: missing repository")
|
||||
return "", errors.New("invalid OCI reference: missing repository")
|
||||
}
|
||||
|
||||
// Get the last part of the repository path as the plugin name
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@ limitations under the License.
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"oras.land/oras-go/v2/registry"
|
||||
@@ -80,5 +79,5 @@ func (r *reference) String() string {
|
||||
|
||||
// IsOCI determines whether a URL is to be treated as an OCI URL
|
||||
func IsOCI(url string) bool {
|
||||
return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
|
||||
return strings.HasPrefix(url, OCIScheme+"://")
|
||||
}
|
||||
|
||||
+19
-4
@@ -80,7 +80,7 @@ func (c ChartVersions) Less(a, b int) bool {
|
||||
// IndexFile represents the index file in a chart repository
|
||||
type IndexFile struct {
|
||||
// This is used ONLY for validation against chartmuseum's index files and is discarded after validation.
|
||||
ServerInfo map[string]interface{} `json:"serverInfo,omitempty"`
|
||||
ServerInfo map[string]any `json:"serverInfo,omitempty"`
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Generated time.Time `json:"generated"`
|
||||
Entries map[string]ChartVersions `json:"entries"`
|
||||
@@ -175,6 +175,19 @@ func (i IndexFile) SortEntries() {
|
||||
}
|
||||
}
|
||||
|
||||
// isVersionRange checks if the version string is a range constraint (e.g., "^1", "~1.10")
|
||||
// rather than an exact version (e.g., "1.10.0").
|
||||
func isVersionRange(version string) bool {
|
||||
if strings.ContainsAny(version, "^~<>=!*") || strings.Contains(version, "||") || strings.Contains(version, " - ") {
|
||||
return true
|
||||
}
|
||||
core := version
|
||||
if idx := strings.IndexAny(version, "-+"); idx != -1 {
|
||||
core = version[:idx]
|
||||
}
|
||||
return strings.ContainsAny(core, "xX")
|
||||
}
|
||||
|
||||
// Get returns the ChartVersion for the given name.
|
||||
//
|
||||
// If version is empty, this will return the chart with the latest stable version,
|
||||
@@ -215,8 +228,10 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
|
||||
}
|
||||
|
||||
if constraint.Check(test) {
|
||||
if len(version) != 0 {
|
||||
if len(version) != 0 && !isVersionRange(version) {
|
||||
slog.Warn("unable to find exact version requested; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version)
|
||||
} else if len(version) != 0 && isVersionRange(version) {
|
||||
slog.Debug("selected version matching constraint", "chart", name, "constraint", version, "selected", ver.Version)
|
||||
}
|
||||
return ver, nil
|
||||
}
|
||||
@@ -270,7 +285,7 @@ func (i *IndexFile) Merge(f *IndexFile) {
|
||||
type ChartVersion struct {
|
||||
*chart.Metadata
|
||||
URLs []string `json:"urls"`
|
||||
Created time.Time `json:"created,omitempty"`
|
||||
Created time.Time `json:"created"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
|
||||
@@ -391,7 +406,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
|
||||
// checking its validity as JSON. If the data is valid JSON, it will use the
|
||||
// `encoding/json` package to unmarshal it. Otherwise, it will use the
|
||||
// `sigs.k8s.io/yaml` package to unmarshal the YAML data.
|
||||
func jsonOrYamlUnmarshal(b []byte, i interface{}) error {
|
||||
func jsonOrYamlUnmarshal(b []byte, i any) error {
|
||||
if json.Valid(b) {
|
||||
return json.Unmarshal(b, i)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user