Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add --threshold flag to nomos vet #1580

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(maxN, foundN 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`,
foundN, maxN).
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(maxN int) func([]ast.FileObject) status.MultiError {
if maxN <= 0 {
return noOpValidator
}
return func(objs []ast.FileObject) status.MultiError {
foundN := len(objs)
if foundN > maxN {
return system.MaxObjectCountError(maxN, foundN)
}
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