mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-20 22:52:51 +02:00
add trackLogsInterval option (#2825)
* add trackLogsInterval option Signed-off-by: pznamensky <kompastver@gmail.com> * test: add coverage for trackLogsInterval option The trackLogsInterval feature shipped without tests. Add coverage for: - config getter: zero when --track-logs-interval is not supplied, explicit value (including an explicit default) when supplied, and the nil-Cmd fallback for non-cobra callers (pkg/config/apply_test.go, sync_test.go) - buildReleaseTracker resolution: explicit CLI interval overrides the release setting, release setting used when the flag is absent, 10s default preserved when both are unset (pkg/state/helmx_test.go) - release-level minimum validation in ExecuteTemplates: below/zero/ negative rejected, 1s and above accepted (pkg/state/state_exec_tmpl_test.go) Also add the missing blank line after syncImpl.Cmd in cmd/sync.go for consistency with cmd/apply.go. Signed-off-by: yxxhero <aiopsclub@163.com> --------- Signed-off-by: pznamensky <kompastver@gmail.com> Signed-off-by: yxxhero <aiopsclub@163.com> Co-authored-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
+2
-1
@@ -16,6 +16,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
Short: "Apply all resources from state file only when there are changes",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
applyImpl := config.NewApplyImpl(globalCfg, applyOptions)
|
||||
applyImpl.Cmd = cmd
|
||||
|
||||
err := config.NewCLIConfigImpl(applyImpl.GlobalImpl)
|
||||
if err != nil {
|
||||
@@ -77,7 +78,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
f.BoolVar(&applyOptions.TrackLogsUntilReady, "track-logs-until-ready", false, "Stop pod log streaming when each pod becomes ready (requires --track-logs)")
|
||||
f.BoolVar(&applyOptions.TrackFailedLogs, "track-failed-logs", false, "Enable log streaming with kubedog tracking, but only emit logs for pods that enter a failed state. Overridden by --track-logs when both are set")
|
||||
f.DurationVar(&applyOptions.TrackLogsInterval, "track-logs-interval", applyOptions.TrackLogsInterval,
|
||||
"Interval between kubedog log output updates (minimum 1s)")
|
||||
"Interval between kubedog log output updates (minimum 1s); overrides trackLogsInterval")
|
||||
f.IntVar(&applyOptions.HelmStuckGrace, "helm-stuck-grace", 0, "When using --track-mode kubedog: if the cluster confirms all tracked resources have converged but the helm subprocess is still running, wait this many seconds before sending SIGINT to helm. Recovers from helm v4 hook waiter wedges. May leave the release secret in pending-install state requiring manual cleanup. 0 disables.")
|
||||
f.BoolVar(&applyOptions.TrackFailOnError, "track-fail-on-error", false, "Fail with non-zero exit code when kubedog tracking fails")
|
||||
f.StringVar(&applyOptions.Description, "description", "", `Set description for all releases. If set, overridesdescriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`)
|
||||
|
||||
+3
-1
@@ -16,6 +16,8 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
Short: "Sync releases defined in state file",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
syncImpl := config.NewSyncImpl(globalCfg, syncOptions)
|
||||
syncImpl.Cmd = cmd
|
||||
|
||||
err := config.NewCLIConfigImpl(syncImpl.GlobalImpl)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -61,7 +63,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
f.BoolVar(&syncOptions.TrackLogsUntilReady, "track-logs-until-ready", false, "Stop pod log streaming when each pod becomes ready (requires --track-logs)")
|
||||
f.BoolVar(&syncOptions.TrackFailedLogs, "track-failed-logs", false, "Enable log streaming with kubedog tracking, but only emit logs for pods that enter a failed state. Overridden by --track-logs when both are set")
|
||||
f.DurationVar(&syncOptions.TrackLogsInterval, "track-logs-interval", syncOptions.TrackLogsInterval,
|
||||
"Interval between kubedog log output updates (minimum 1s)")
|
||||
"Interval between kubedog log output updates (minimum 1s); overrides trackLogsInterval")
|
||||
f.IntVar(&syncOptions.HelmStuckGrace, "helm-stuck-grace", 0, "When using --track-mode kubedog: if the cluster confirms all tracked resources have converged but the helm subprocess is still running, wait this many seconds before sending SIGINT to helm. Recovers from helm v4 hook waiter wedges. May leave the release secret in pending-install state requiring manual cleanup. 0 disables.")
|
||||
f.BoolVar(&syncOptions.TrackFailOnError, "track-fail-on-error", false, "Fail with non-zero exit code when kubedog tracking fails")
|
||||
f.StringVar(&syncOptions.Description, "description", "", `Set description for all releases. If set, overrides descriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`)
|
||||
|
||||
@@ -34,12 +34,13 @@ helmfile apply --track-mode kubedog --track-timeout 300 --track-logs --track-log
|
||||
- **`trackMode`**: Set to `kubedog` to enable kubedog tracking, or `helm-legacy` to use Helm v4's legacy wait mode (default: `helm`)
|
||||
- **`trackTimeout`**: Timeout in seconds for tracking resources (default: 300)
|
||||
- **`trackLogs`**: Print logs from tracked resources during deployment
|
||||
- **`trackLogsInterval`**: Interval between log output updates, as a duration such as `1s` or `30s` (default: `10s`; minimum: `1s`)
|
||||
- **`trackLogsUntilReady`**: With `trackLogs: true`, stop routine pod logs once each pod first becomes ready (default: false)
|
||||
- **`trackFailedLogs`**: Print collected logs only for pods that fail during deployment
|
||||
|
||||
To see logs only for failed pods, set `trackFailedLogs: true` on a release or use `--track-failed-logs` with `helmfile apply` or `helmfile sync`. If both log options are enabled, logs from all tracked pods are printed.
|
||||
|
||||
With kubedog tracking and either log option enabled, `helmfile apply` and `helmfile sync` print collected logs every 10 seconds by default. Use `--track-logs-interval` to change this interval (for example, `1s` above). The minimum is `1s`. Deployment progress is checked for changes every 10 seconds.
|
||||
With kubedog tracking and either `trackLogs` or `trackFailedLogs` enabled, `helmfile apply` and `helmfile sync` print collected logs every 10 seconds by default. Set `trackLogsInterval` on a release or use `--track-logs-interval` to change this interval (for example, `1s` above). An explicitly supplied command-line flag overrides the release setting; otherwise, the release setting takes precedence over the `10s` default. The minimum is `1s`. Deployment progress is checked for changes every 10 seconds.
|
||||
|
||||
### Startup-Only Pod Logs
|
||||
|
||||
|
||||
@@ -514,7 +514,8 @@ See [Advanced Features](advanced-features.md#resource-tracking-with-kubedog) for
|
||||
|-------|------|---------|-------------|
|
||||
| `trackMode` | string | `""` | Track mode: `helm`, `helm-legacy`, or `kubedog` |
|
||||
| `trackTimeout` | int | 300 | Tracking timeout in seconds |
|
||||
| `trackLogs` | bool | false | Print logs from tracked resources during deployment (every 10 seconds by default; see [`--track-logs-interval`](advanced-features.md#resource-tracking-with-kubedog)) |
|
||||
| `trackLogs` | bool | false | Print logs from tracked resources during deployment |
|
||||
| `trackLogsInterval` | duration | `10s` | Interval between log output updates for `trackLogs` or `trackFailedLogs` (e.g. `1s`; minimum `1s`); overrides the default, but an explicitly supplied `--track-logs-interval` takes precedence |
|
||||
| `trackLogsUntilReady` | bool | false | With `trackLogs: true`, stop routine pod logs once each pod first becomes ready; readiness and failure tracking continue. Job logs are unaffected |
|
||||
| `trackFailedLogs` | bool | false | Print collected logs only for pods that fail during deployment; overridden by `trackLogs` |
|
||||
| `trackKinds` | list | | Whitelist of resource kinds to track |
|
||||
|
||||
+7
-1
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ApplyOptoons is the options for the apply command
|
||||
@@ -121,6 +123,7 @@ func NewApplyOptions() *ApplyOptions {
|
||||
type ApplyImpl struct {
|
||||
*GlobalImpl
|
||||
*ApplyOptions
|
||||
Cmd *cobra.Command
|
||||
}
|
||||
|
||||
// NewApplyImpl creates a new ApplyImpl
|
||||
@@ -356,8 +359,11 @@ func (a *ApplyImpl) TrackFailedLogs() bool {
|
||||
return a.ApplyOptions.TrackFailedLogs
|
||||
}
|
||||
|
||||
// TrackLogsInterval returns the kubedog log output interval.
|
||||
// TrackLogsInterval returns the CLI interval, or zero when the flag was not supplied.
|
||||
func (a *ApplyImpl) TrackLogsInterval() time.Duration {
|
||||
if a.Cmd != nil && !a.Cmd.Flags().Changed("track-logs-interval") {
|
||||
return 0
|
||||
}
|
||||
return a.ApplyOptions.TrackLogsInterval
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplyImpl_TrackLogsInterval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setFlag string // explicit --track-logs-interval value; empty means not supplied
|
||||
noCmd bool // construct ApplyImpl without a Cmd, like non-cobra callers
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "zero when flag not supplied",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "explicit value returned when flag supplied",
|
||||
setFlag: "3s",
|
||||
want: 3 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "explicit default value still counts as supplied",
|
||||
setFlag: "10s",
|
||||
want: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "nil cmd falls back to option value",
|
||||
noCmd: true,
|
||||
want: defaultTrackLogsInterval,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := NewApplyOptions()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().DurationVar(&options.TrackLogsInterval, "track-logs-interval", options.TrackLogsInterval, "")
|
||||
if tt.setFlag != "" {
|
||||
require.NoError(t, cmd.Flags().Set("track-logs-interval", tt.setFlag))
|
||||
}
|
||||
|
||||
impl := NewApplyImpl(nil, options)
|
||||
if !tt.noCmd {
|
||||
impl.Cmd = cmd
|
||||
}
|
||||
require.Equal(t, tt.want, impl.TrackLogsInterval())
|
||||
})
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// defaultTrackLogsInterval is the default --track-logs-interval shared by
|
||||
@@ -110,6 +112,7 @@ func NewSyncOptions() *SyncOptions {
|
||||
type SyncImpl struct {
|
||||
*GlobalImpl
|
||||
*SyncOptions
|
||||
Cmd *cobra.Command
|
||||
}
|
||||
|
||||
// NewSyncImpl creates a new SyncImpl
|
||||
@@ -269,8 +272,11 @@ func (t *SyncImpl) TrackFailedLogs() bool {
|
||||
return t.SyncOptions.TrackFailedLogs
|
||||
}
|
||||
|
||||
// TrackLogsInterval returns the kubedog log output interval.
|
||||
// TrackLogsInterval returns the CLI interval, or zero when the flag was not supplied.
|
||||
func (t *SyncImpl) TrackLogsInterval() time.Duration {
|
||||
if t.Cmd != nil && !t.Cmd.Flags().Changed("track-logs-interval") {
|
||||
return 0
|
||||
}
|
||||
return t.SyncOptions.TrackLogsInterval
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSyncImpl_TrackLogsInterval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setFlag string // explicit --track-logs-interval value; empty means not supplied
|
||||
noCmd bool // construct SyncImpl without a Cmd, like non-cobra callers
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "zero when flag not supplied",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "explicit value returned when flag supplied",
|
||||
setFlag: "3s",
|
||||
want: 3 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "explicit default value still counts as supplied",
|
||||
setFlag: "10s",
|
||||
want: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "nil cmd falls back to option value",
|
||||
noCmd: true,
|
||||
want: defaultTrackLogsInterval,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := NewSyncOptions()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().DurationVar(&options.TrackLogsInterval, "track-logs-interval", options.TrackLogsInterval, "")
|
||||
if tt.setFlag != "" {
|
||||
require.NoError(t, cmd.Flags().Set("track-logs-interval", tt.setFlag))
|
||||
}
|
||||
|
||||
impl := NewSyncImpl(nil, options)
|
||||
if !tt.noCmd {
|
||||
impl.Cmd = cmd
|
||||
}
|
||||
require.Equal(t, tt.want, impl.TrackLogsInterval())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -338,6 +338,8 @@ func (st *HelmState) buildReleaseTracker(release *ReleaseSpec, opts *SyncOpts, u
|
||||
WithColor(useColor)
|
||||
if opts != nil && opts.TrackLogsInterval > 0 {
|
||||
trackOpts.WithLogsInterval(opts.TrackLogsInterval)
|
||||
} else if release.TrackLogsInterval != nil {
|
||||
trackOpts.WithLogsInterval(*release.TrackLogsInterval)
|
||||
}
|
||||
|
||||
tracker, err := kubedog.NewTracker(&kubedog.TrackerConfig{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -859,3 +860,58 @@ current-context: test
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildReleaseTracker_LogsInterval(t *testing.T) {
|
||||
kubeconfig := filepath.Join(t.TempDir(), "kubeconfig")
|
||||
require.NoError(t, os.WriteFile(kubeconfig, []byte(`apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- name: test
|
||||
cluster:
|
||||
server: http://127.0.0.1:1
|
||||
contexts:
|
||||
- name: test
|
||||
context:
|
||||
cluster: test
|
||||
current-context: test
|
||||
`), 0600))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
release string
|
||||
opts *SyncOpts
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "10s default when unset",
|
||||
want: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "explicit cli interval overrides release setting",
|
||||
release: "trackLogsInterval: 2s\n",
|
||||
opts: &SyncOpts{TrackLogsInterval: 30 * time.Second},
|
||||
want: 30 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "release interval used when cli flag absent",
|
||||
release: "trackLogsInterval: 2s\n",
|
||||
want: 2 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "cli interval used when release unset",
|
||||
opts: &SyncOpts{TrackLogsInterval: 3 * time.Second},
|
||||
want: 3 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var release ReleaseSpec
|
||||
require.NoError(t, yaml.Unmarshal([]byte(tt.release), &release))
|
||||
st := &HelmState{kubeconfig: kubeconfig}
|
||||
_, opts, err := st.buildReleaseTracker(&release, tt.opts, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want, opts.LogsInterval)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,6 +574,8 @@ type ReleaseSpec struct {
|
||||
TrackTimeout *int `yaml:"trackTimeout,omitempty"`
|
||||
// TrackLogs enables log streaming with kubedog
|
||||
TrackLogs *bool `yaml:"trackLogs,omitempty"`
|
||||
// TrackLogsInterval specifies how often captured logs are printed. The minimum is 1s.
|
||||
TrackLogsInterval *time.Duration `yaml:"trackLogsInterval,omitempty"`
|
||||
// TrackLogsUntilReady stops pod log streaming when each pod becomes ready. Requires TrackLogs.
|
||||
TrackLogsUntilReady *bool `yaml:"trackLogsUntilReady,omitempty"`
|
||||
// TrackFailedLogs streams logs only for pods that enter a failed state
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dario.cat/mergo"
|
||||
|
||||
@@ -147,6 +148,9 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
|
||||
if st.Releases[i].Chart == "" {
|
||||
return nil, fmt.Errorf("encountered empty chart while reading release %q", st.Releases[i].Name)
|
||||
}
|
||||
if interval := st.Releases[i].TrackLogsInterval; interval != nil && *interval < time.Second {
|
||||
return nil, fmt.Errorf("release %q: trackLogsInterval must be at least 1s, got: %s", st.Releases[i].Name, *interval)
|
||||
}
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/environment"
|
||||
@@ -277,6 +279,65 @@ func TestHelmState_executeTemplates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_executeTemplatesTrackLogsIntervalValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
interval time.Duration
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "minimum 1s is valid",
|
||||
interval: time.Second,
|
||||
},
|
||||
{
|
||||
name: "above minimum is valid",
|
||||
interval: 5 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "below minimum is rejected",
|
||||
interval: 500 * time.Millisecond,
|
||||
wantErr: `release "foo": trackLogsInterval must be at least 1s, got: 500ms`,
|
||||
},
|
||||
{
|
||||
name: "zero is rejected",
|
||||
interval: 0,
|
||||
wantErr: `release "foo": trackLogsInterval must be at least 1s, got: 0s`,
|
||||
},
|
||||
{
|
||||
name: "negative is rejected",
|
||||
interval: -time.Second,
|
||||
wantErr: `release "foo": trackLogsInterval must be at least 1s, got: -1s`,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
interval := tt.interval
|
||||
st := &HelmState{
|
||||
fs: &filesystem.FileSystem{Glob: func(s string) ([]string, error) { return nil, nil }},
|
||||
basePath: ".",
|
||||
Env: environment.Environment{Name: "test_env"},
|
||||
Releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "foo",
|
||||
Chart: "incubator/raw",
|
||||
TrackLogsInterval: &interval,
|
||||
},
|
||||
},
|
||||
RenderedValues: map[string]any{},
|
||||
}
|
||||
|
||||
_, err := st.ExecuteTemplates()
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_recursiveRefsTemplates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -42,39 +42,39 @@ func TestGenerateID(t *testing.T) {
|
||||
run(testcase{
|
||||
subject: "baseline",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||
want: "foo-values-699f8dfb8f",
|
||||
want: "foo-values-66b6b57c5",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "different bytes content",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||
data: []byte(`{"k":"v"}`),
|
||||
want: "foo-values-8484b97b54",
|
||||
want: "foo-values-764568cb",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "different map content",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||
data: map[string]any{"k": "v"},
|
||||
want: "foo-values-6d74854796",
|
||||
want: "foo-values-78dcc498c4",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "different chart",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
|
||||
want: "foo-values-6c75d94b64",
|
||||
want: "foo-values-788fc9bdf8",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "different name",
|
||||
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
|
||||
want: "bar-values-fdc66db9d",
|
||||
want: "bar-values-6b4546d548",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "specific ns",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
|
||||
want: "myns-foo-values-74d9576cdb",
|
||||
want: "myns-foo-values-75b997c6dd",
|
||||
})
|
||||
|
||||
for id, n := range ids {
|
||||
|
||||
Reference in New Issue
Block a user