controller-tools icon indicating copy to clipboard operation
controller-tools copied to clipboard

[BUG] Automatically generated regex validation for `Quantity` does not match the validation used by unmarshalerDecoder

Open hoyhbx opened this issue 2 years ago • 12 comments

rabbitmq's cluster-operator(https://github.com/rabbitmq/cluster-operator) and a lot of other operators use controller-gen to automatically generate their CRDs. For the Quantity field, controller-gen automatically generates the following JSON schema:

additionalProperties:
  anyOf:
  - type: integer
  - type: string
  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
  x-kubernetes-int-or-string: true
description: 'Limits describes the maximum amount of compute resources
  allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
type: object

However, the automatically generated pattern does not match the validation pattern used by the unmarshalerDecoder, causing cases where the value is valid against the CRD schema, but invalid against the unmarshalerDecoder. This causes the invalid inputs being silently rejected by the operator, and all the subsequent inputs are silently rejected without fixing the invalid value.

Example

Concretely, when specifying the value as .07893E985.90504 for the field Quantity, there will be an error message from client-go shown in the operator's log:

E0421 07:19:52.910713       1 reflector.go:138] pkg/mod/k8s.io/[email protected]/tools/cache/reflector.go:167: Failed to watch *v1beta1.RabbitmqCluster: failed to list *v1beta1.RabbitmqCluster: v1beta1.RabbitmqClusterList.Items: []v1beta1.RabbitmqCluster: v1beta1.RabbitmqCluster.Spec: v1beta1.RabbitmqClusterSpec.Rabbitmq: v1beta1.RabbitmqClusterConfigurationSpec.Persistence: v1beta1.RabbitmqClusterPersistenceSpec.Storage: unmarshalerDecoder: quantities must match the regular expression '^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$', error found in #10 byte of ...|985.90504"},"rabbitm|..., bigger context ...|de":{},"persistence":{"storage":".07893E985.90504"},"rabbitmq":{"additionalConfig":"cluster_partitio|...

The value .07893E985.90504 satisfies the regex generated by controller-gen ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$, but does not satisfy the regex required by the unmarshalerDecoder: ^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$. The input CR is silently rejected by the operator without any error message from kubectl, and all the subsequent CRs are rejected until the invalid value is fixed.

Additional comments

The regex validation generated by controller-gen seems to be looser than the validation used by the unmarshalerDecoder, which causes the apiserver fails to reject the invalid values.

Would it make more sense to changed the regex generated by controller-gen to match the one used by the unmarshalerDecoder here?: https://github.com/kubernetes-sigs/controller-tools/blob/8cb5ce83c3cca425a4de0af3d2578e31a3cd6a48/pkg/crd/known_types.go#L69

hoyhbx avatar Apr 26 '22 19:04 hoyhbx

Yeah, should match what upstreams uses. Feel free to open a PR where you update it and reference the upstream source.

alvaroaleman avatar Apr 30 '22 14:04 alvaroaleman

Unfortunately, upstream does not use a regexp for validation so it can’t be simply included: https://github.com/kubernetes/apimachinery/blob/3b8fb46ed6f145544bd363fab539decff47c3753/pkg/api/resource/quantity.go#L147

Porges avatar May 02 '22 23:05 Porges

The validation does not come from the kubernetes' parsing, but from a component called unmarshalerDecoder. I am still trying to find where exactly this component is implemented, but there is definitely some validation being used according to the error message from client-go.

hoyhbx avatar May 03 '22 17:05 hoyhbx

Unmarshaler decoder is here: https://github.com/kubernetes/kubernetes/blob/f02682c628c530219966a00ae002d799f0d813dc/vendor/github.com/json-iterator/go/reflect_marshaler.go#L201

Regex is here but it is documented as being overly permissive: https://github.com/kubernetes/kubernetes/blob/f02682c628c530219966a00ae002d799f0d813dc/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity.go#L136

If it is an improvement over what we currently have, we should probably still update - But first look into the history of what we have today and how it came to be.

alvaroaleman avatar May 03 '22 17:05 alvaroaleman

Thanks for the pointers, sorry I didn't see the error message that the parseQuantityString was returning. I think we should use a regex that conform to the validation used by the upstream, and the current one used by controller-gen does not because it's over permissive. The question is if the one in the upstream is good enough or there is a better regex that can express the validation better.

hoyhbx avatar May 03 '22 18:05 hoyhbx

I'd also note that the documentation of what is permitted is incorrect with regards to what parseQuantityString actually allows. (e.g. n and u are permitted as suffixes)

Porges avatar May 03 '22 21:05 Porges

Here's a first-pass take on a regex that matches upstream:

package tests

import (
	"fmt"
	"regexp"
	"testing"
)

// copied from: https://github.com/kubernetes/apimachinery/blob/3b8fb46ed6f145544bd363fab539decff47c3753/pkg/api/resource/quantity.go#L31-L49

// <digit>           ::= 0 | 1 | ... | 9
// <digits>          ::= <digit> | <digit><digits>
// <number>          ::= <digits> | <digits>.<digits> | <digits>. | .<digits>
var number = `((\.[0-9]+)|([0-9](\.[0-9]+)?)|([0-9]+\.))`

// <sign>            ::= "+" | "-"
var sign = `[-+]`

// <signedNumber>    ::= <number> | <sign><number>
var signedNumber = sign + "?" + number

// <binarySI>        ::= Ki | Mi | Gi | Ti | Pi | Ei
var binarySI = `([KMGTPE]i)`

// <decimalSI>       ::= m | "" | k | M | G | T | P | E
var decimalSI = `([numkMGTPE]|)` // note empty permitted, and also 'u' and 'n' are permitted, contrary to the description, see: https://github.com/kubernetes/apimachinery/blob/57f2a0733447cfd41294477d833cce6580faaca3/pkg/api/resource/suffix.go#L108-L135

// <decimalExponent> ::= "e" <signedNumber> | "E" <signedNumber>
var decimalExponent = `([eE]` + signedNumber + `)`

// <suffix>          ::= <binarySI> | <decimalExponent> | <decimalSI>
var suffix = `(` + binarySI + `|` + decimalExponent + `|` + decimalSI + `)`

// <quantity>        ::= <signedNumber><suffix>
var quantity = `^` + signedNumber + suffix + `$`

var quantityRegexp = regexp.MustCompile(quantity)

func ExampleFullForm() {
	fmt.Println(quantity)

	// Output:
	// ^[-+]?((\.[0-9]+)|([0-9](\.[0-9]+)?)|([0-9]+\.))(([KMGTPE]i)|([eE][-+]?((\.[0-9]+)|([0-9](\.[0-9]+)?)|([0-9]+\.)))|([mkMGTPE]|))$
}

func TestQuantityRegexpValid(t *testing.T) {
	validInputs := []string{
		"+1",
		"-1",
		"1",
		"1.",
		".1",
		"1.1",
		"12e6",
		"12E6",
		"12e-6",
		"12E-6",
		"12e+6",
		"12E+6",
		"12Mi",
		"12M",
		"0.1",
		"0.12",
		"0.123",
		"1u",
		"1n",
	}

	for _, validInput := range validInputs {
		if !quantityRegexp.MatchString(validInput) {
			t.Errorf("Input '%s' should be valid", validInput)
		}
	}
}

func TestQuantityRegexpInvalid(t *testing.T) {
	validInputs := []string{
		"1.x",
		"x.1",
		".",
		"12e--6",
		"12E++6",
		"12MiMi",
                "",
	}

	for _, validInput := range validInputs {
		if quantityRegexp.MatchString(validInput) {
			t.Errorf("Input '%s' should be invalid", validInput)
		}
	}
}

Porges avatar May 03 '22 21:05 Porges

I'd also note that the documentation of what is permitted is incorrect with regards to what parseQuantityString actually allows. (e.g. n and u are permitted as suffixes)

Yeah I agree. And the parseQuantityString does not conform to the decimalExponent production rule. The production rule permits signed number, but parseQuantityString seems to only permit signed integer: https://github.com/kubernetes/kubernetes/blob/f02682c628c530219966a00ae002d799f0d813dc/staging/src/k8s.io/apimachinery/pkg/api/resource/quantity.go#L252, which makes .07893E985.90504 to fail the validation.

We should probably report to upstream of this inconsistency and hear their opinion.

hoyhbx avatar May 04 '22 20:05 hoyhbx

Hi @Porges , I realized the regex above has a little typo. The regex for number should be

var number = `((\.[0-9]+)|([0-9]+(\.[0-9]+)?)|([0-9]+\.))`

instead of

var number = `((\.[0-9]+)|([0-9](\.[0-9]+)?)|([0-9]+\.))`

Otherwise, 992m would fail the validation

hoyhbx avatar Jun 14 '22 20:06 hoyhbx

The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue or PR as fresh with /remove-lifecycle stale
  • Mark this issue or PR as rotten with /lifecycle rotten
  • Close this issue or PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle stale

k8s-triage-robot avatar Sep 12 '22 21:09 k8s-triage-robot

/remove-lifecycle stale

hoyhbx avatar Sep 19 '22 15:09 hoyhbx

The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue or PR as fresh with /remove-lifecycle stale
  • Mark this issue or PR as rotten with /lifecycle rotten
  • Close this issue or PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle stale

k8s-triage-robot avatar Dec 18 '22 15:12 k8s-triage-robot

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue or PR as fresh with /remove-lifecycle rotten
  • Close this issue or PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle rotten

k8s-triage-robot avatar Jan 17 '23 16:01 k8s-triage-robot

The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue as fresh with /remove-lifecycle stale
  • Close this issue with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle stale

k8s-triage-robot avatar May 01 '23 10:05 k8s-triage-robot

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Mark this issue as fresh with /remove-lifecycle rotten
  • Close this issue with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle rotten

k8s-triage-robot avatar May 31 '23 11:05 k8s-triage-robot

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Reopen this issue with /reopen
  • Mark this issue as fresh with /remove-lifecycle rotten
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/close not-planned

k8s-triage-robot avatar Jun 30 '23 11:06 k8s-triage-robot

@k8s-triage-robot: Closing this issue, marking it as "Not Planned".

In response to this:

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the issue is closed

You can:

  • Reopen this issue with /reopen
  • Mark this issue as fresh with /remove-lifecycle rotten
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/close not-planned

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

k8s-ci-robot avatar Jun 30 '23 11:06 k8s-ci-robot

lol. lmao.

Porges avatar Jul 04 '23 05:07 Porges