Skip to content

Commit

Permalink
feat: Add --threshold flag to nomos vet
Browse files Browse the repository at this point in the history
- Added a new `--threshold[=MAX]` option to the `nomos vet` command
  that allows specifying a maximum number of objects. If set, it will
  enable validation that will error if the number of objects exceeds
  the specified value, after rendering and cluster selectors.
  By default, this validation is disabled. If you pass the option
  without a value, the default value of 1000 is used.
- The value 1000 was chosen as a compromise between scale and safety.
  Technically we know form e2e tests that the inventory can actually
  hold at least 5000 objects without needing to disable the status,
  however, this is unsafe to do in production because each of those
  objects could error, which adds error conditions to the inventory
  status, which can significantly increase the size of the inventory.
- Unlike other options, this option has an optional value and
  different defaults when specified than when not specified. This
  requires the flag and value to be sent in the same argument, like
  `--threshold=1000`, instead of as two different arguments, like
  `--threshold 1000`.
- If the option is specified with a value of zero or lower, the
  validation will be disabled, the same as if the option was not
  specified.
  • Loading branch information
karlkfi committed Mar 1, 2025
1 parent 9eae3e6 commit 8eee45e
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 9 deletions.
16 changes: 15 additions & 1 deletion cmd/nomos/vet/vet.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import (
"github.com/spf13/cobra"
"kpt.dev/configsync/cmd/nomos/flags"
"kpt.dev/configsync/pkg/api/configsync"
"kpt.dev/configsync/pkg/importer/analyzer/validation/system"
)

var (
namespaceValue string
keepOutput bool
threshold int
outPath string
)

Expand All @@ -43,6 +45,13 @@ func init() {
Cmd.Flags().BoolVar(&keepOutput, "keep-output", false,
`If enabled, keep the hydrated output`)

Cmd.Flags().IntVar(&threshold, "threshold", system.DefaultMaxObjectCount,
`If greater than zero, error if any repository contains more than the specified number of objects `)
// Set value to zero when `--threshold` is passed without a value.
// This requires the flag and value to be sent in the same argument, like
// `--threshold=1000`, instead of `--threshold 1000`.
Cmd.Flags().Lookup("threshold").NoOptDefVal = "0"

Cmd.Flags().StringVar(&outPath, "output", flags.DefaultHydrationOutput,
`Location of the hydrated output`)
}
Expand All @@ -64,6 +73,11 @@ returns a non-zero error code if any issues are found.
// Don't show usage on error, as argument validation passed.
cmd.SilenceUsage = true

return runVet(cmd.Context(), namespaceValue, configsync.SourceFormat(flags.SourceFormat), flags.APIServerTimeout)
return runVet(cmd.Context(), vetOptions{
Namespace: namespaceValue,
SourceFormat: configsync.SourceFormat(flags.SourceFormat),
APIServerTimeout: flags.APIServerTimeout,
MaxObjectCount: threshold,
})
},
}
14 changes: 12 additions & 2 deletions cmd/nomos/vet/vet_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ import (
"kpt.dev/configsync/pkg/status"
)

type vetOptions struct {
Namespace string
SourceFormat configsync.SourceFormat
APIServerTimeout time.Duration
MaxObjectCount int
}

// vet runs nomos vet with the specified options.
//
// root is the OS-specific path to the Nomos policy root.
Expand All @@ -52,7 +59,9 @@ import (
// clusters is the set of clusters we are checking.
//
// Only used if allClusters is false.
func runVet(ctx context.Context, namespace string, sourceFormat configsync.SourceFormat, apiServerTimeout time.Duration) error {
func runVet(ctx context.Context, opts vetOptions) error {
namespace := opts.Namespace
sourceFormat := opts.SourceFormat
if sourceFormat == "" {
if namespace == "" {
// Default to hierarchical if --namespace is not provided.
Expand Down Expand Up @@ -86,11 +95,12 @@ func runVet(ctx context.Context, namespace string, sourceFormat configsync.Sourc

parser := filesystem.NewParser(&reader.File{})

validateOpts, err := hydrate.ValidateOptions(ctx, rootDir, apiServerTimeout)
validateOpts, err := hydrate.ValidateOptions(ctx, rootDir, opts.APIServerTimeout)
if err != nil {
return err
}
validateOpts.FieldManager = util.FieldManager
validateOpts.MaxObjectCount = opts.MaxObjectCount

switch sourceFormat {
case configsync.SourceFormatHierarchy:
Expand Down
3 changes: 3 additions & 0 deletions cmd/nomoserrors/examples/examples.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ func Generate() AllExamples {
// 1069
result.add(validate.SelfReconcileError(k8sobjects.RootSyncV1Beta1(configsync.RootSyncName)))

// 1070
result.add(system.MaxObjectCountError(system.DefaultMaxObjectCount, system.DefaultMaxObjectCount+1))

// 2001
result.add(status.PathWrapError(errors.New("error creating directory"), "namespaces/foo"))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2025 Google LLC
//
// 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 system

import (
"kpt.dev/configsync/pkg/status"
)

// DefaultMaxObjectCount is the default maximum number of objects allowed in a
// single inventory, if the validator is enabled.
//
// This is only used by nomos vet, not the reconciler. It will not block syncing.
//
// The value 1000 was chosen as a compromise between scale and safety.
// Technically we know form e2e tests that the inventory can actually hold at
// least 5000 objects without needing to disable the status, however, this is
// unsafe to do in production because each of those objects could error, which
// adds error conditions to the inventory status, which can significantly
// increase the size of the inventory.
const DefaultMaxObjectCount int = 1000

// MaxObjectCountCode is the error code for MaxObjectCount
const MaxObjectCountCode = "1070"

var maxObjectCountErrorBuilder = status.NewErrorBuilder(MaxObjectCountCode)

// MaxObjectCountError reports that the source includes more than the maximum
// number of objects.
func MaxObjectCountError(max, found int) status.Error {
return maxObjectCountErrorBuilder.
Sprintf(`Maximum number of objects exceeded. Found %d, but expected no more than %d. `+
`Reduce the number of objects being synced to this cluster in your source of truth `+
`to prevent your ResourceGroup inventory object from exceeding the etcd object size limit. `+
`For instructions on how to break up a repository into multiple repositories, see `+
`https://cloud.google.com/kubernetes-engine/enterprise/config-sync/docs/how-to/breaking-up-repo`,
found, max).
Build()
}
16 changes: 12 additions & 4 deletions pkg/validate/final/final.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,25 @@ import (
"kpt.dev/configsync/pkg/validate/final/validate"
)

type finalValidator func(objs []ast.FileObject) status.MultiError
// Options used to configure the Validate function
type Options struct {
// MaxObjectCount is the maximum number of objects allowed in a single
// inventory. Validation is skipped when less than 1.
MaxObjectCount int
}

type validator func(objs []ast.FileObject) status.MultiError

// Validation performs final validation checks against the given FileObjects.
// Validate performs final validation checks against the given FileObjects.
// This should be called after all hydration steps are complete so that it can
// validate the final state of the repo.
func Validation(objs []ast.FileObject) status.MultiError {
func Validate(objs []ast.FileObject, opts Options) status.MultiError {
var errs status.MultiError
// See the note about ordering in raw.Hierarchical().
validators := []finalValidator{
validators := []validator{
validate.DuplicateNames,
validate.UnmanagedNamespaces,
validate.MaxObjectCount(opts.MaxObjectCount),
}
for _, validator := range validators {
errs = status.Append(errs, validator(objs))
Expand Down
40 changes: 40 additions & 0 deletions pkg/validate/final/validate/max_object_count_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2025 Google LLC
//
// 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 validate

import (
"kpt.dev/configsync/pkg/importer/analyzer/ast"
"kpt.dev/configsync/pkg/importer/analyzer/validation/system"
"kpt.dev/configsync/pkg/status"
)

// MaxObjectCount verifies that the number of managed resources does not exceed
// the specified maximum.
func MaxObjectCount(max int) func([]ast.FileObject) status.MultiError {
if max <= 0 {
return noOpValidator
}
return func(objs []ast.FileObject) status.MultiError {
found := len(objs)
if found > max {
return system.MaxObjectCountError(max, found)
}
return nil
}
}

func noOpValidator([]ast.FileObject) status.MultiError {
return nil
}
15 changes: 13 additions & 2 deletions pkg/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ type Options struct {
WebhookEnabled bool
// FieldManager to use when performing cluster operations
FieldManager string
// MaxObjectCount is the maximum number of objects allowed in a single
// inventory. Validation is skipped when less than 1.
MaxObjectCount int
}

// Hierarchical validates and hydrates the given FileObjects from a structured,
Expand Down Expand Up @@ -158,8 +161,12 @@ func Hierarchical(objs []ast.FileObject, opts Options) ([]ast.FileObject, status
// depends on the final state of the objects. This includes:
// - checking for resources with duplicate GKNNs
// - checking for managed resources in unmanaged namespaces
// - checking for too many objects, if configured
finalOpts := final.Options{
MaxObjectCount: opts.MaxObjectCount,
}
finalObjects := treeObjects.Objects()
if errs = final.Validation(finalObjects); errs != nil {
if errs = final.Validate(finalObjects, finalOpts); errs != nil {
return nil, status.Append(nonBlockingErrs, errs)
}

Expand Down Expand Up @@ -233,8 +240,12 @@ func Unstructured(ctx context.Context, c client.Client, objs []ast.FileObject, o
// depends on the final state of the objects. This includes:
// - checking for resources with duplicate GKNNs
// - checking for managed resources in unmanaged namespaces
// - checking for too many objects, if configured
finalOpts := final.Options{
MaxObjectCount: opts.MaxObjectCount,
}
finalObjects := scopedObjects.Objects()
if errs := final.Validation(finalObjects); errs != nil {
if errs := final.Validate(finalObjects, finalOpts); errs != nil {
return nil, status.Append(nonBlockingErrs, errs)
}

Expand Down

0 comments on commit 8eee45e

Please sign in to comment.