-
Notifications
You must be signed in to change notification settings - Fork 89
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
Generate CEL validation rules not to enforce required fields when ObserveOnly #166
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import ( | |
"regexp" | ||
"strings" | ||
"text/template" | ||
"text/template/parse" | ||
|
||
"github.com/crossplane/crossplane-runtime/pkg/errors" | ||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath" | ||
|
@@ -47,6 +48,8 @@ var ( | |
GetIDFn: ExternalNameAsID, | ||
DisableNameInitializer: true, | ||
} | ||
|
||
parameterPattern = regexp.MustCompile(`{{\s*\.parameters\.([^\s}]+)\s*}}`) | ||
) | ||
|
||
// ParameterAsIdentifier uses the given field name in the arguments as the | ||
|
@@ -60,6 +63,7 @@ func ParameterAsIdentifier(param string) ExternalName { | |
param, | ||
param + "_prefix", | ||
} | ||
e.IdentifierFields = []string{param} | ||
return e | ||
} | ||
|
||
|
@@ -90,6 +94,18 @@ func TemplatedStringAsIdentifier(nameFieldPath, tmpl string) ExternalName { | |
if err != nil { | ||
panic(errors.Wrap(err, "cannot parse template")) | ||
} | ||
|
||
// Note(turkenh): If a parameter is used in the external name template, | ||
// it is an identifier field. | ||
var identifierFields []string | ||
for _, node := range t.Root.Nodes { | ||
if node.Type() == parse.NodeAction { | ||
match := parameterPattern.FindStringSubmatch(node.String()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I understand, we check the {{ .parameters.<field_name> }} for catching the root level required fields. This will cover some external name configurations if we use this convention. However, as you know, we also have another external name configurations, for example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the case of In upjet, noticed that we need to mark the parameter as identifier in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may not also be able to extract all the argument names from an arbitrary templating expression here (although the logic implemented should cover the most common case of And I also agree we cannot cover all the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Responded in the other thread: #166 (comment) |
||
if len(match) == 2 { | ||
identifierFields = append(identifierFields, match[1]) | ||
} | ||
} | ||
} | ||
return ExternalName{ | ||
SetIdentifierArgumentFn: func(base map[string]any, externalName string) { | ||
if nameFieldPath == "" { | ||
|
@@ -126,6 +142,7 @@ func TemplatedStringAsIdentifier(nameFieldPath, tmpl string) ExternalName { | |
} | ||
return GetExternalNameFromTemplated(tmpl, id.(string)) | ||
}, | ||
IdentifierFields: identifierFields, | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -104,6 +104,12 @@ type ExternalName struct { | |
// assigned by the provider, like AWS VPC where it gets vpc-21kn123 identifier | ||
// and not let you name it. | ||
DisableNameInitializer bool | ||
|
||
// IdentifierFields are the fields that are used to construct external | ||
// resource identifier. We need to know these fields no matter what the | ||
// management policy is including the Observe Only, different from other | ||
// (required) fields. | ||
IdentifierFields []string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little bit worried that we introduce a new external name configuration field without a robust defaulting for it so that we will have to do manual configurations to get proper required checks in place. We have covered some defaulting for this in If this is not properly configured (i.e., if an identifier parameter is not correctly marked as so), we would expect the Terraform validation to catch it but this would still be a compromise in our API quality. Is there a way to extract this information regarding which Terraform configuration arguments are part of the Terraform ID from the native schema? What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Could you point to some examples? Can we configure them as part of rolling the feature out to the providers?
To be clear, if an identifier parameter is not marked correctly, we will replace Default (not Observe Only):
Observe Only:
I agree with your concerns and have already investigated ways to figure this out before introducing a new configuration; however, unfortunately, couldn't find one. I believe this is a similar problem to external name configuration. The information could be inferred from the import documentation, but there is no robust way of automating this without human intervention. So, the best I can find is to leverage that existing configuration to figure the individual parameters out. Happy to try if you have some other ideas. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I mean with anonymous As we have discussed offline, we may start getting reports on cases where we cannot enforce required fields at the Kubernetes API level as you mentioned above and although the remedy is to supply a value for the required field at runtime, we will also want to do a manual configuration on |
||
} | ||
|
||
// References represents reference resolver configurations for the fields of a | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,14 +33,17 @@ type Generated struct { | |
|
||
ForProviderType *types.Named | ||
AtProviderType *types.Named | ||
|
||
ValidationRules string | ||
} | ||
|
||
// Builder is used to generate Go type equivalence of given Terraform schema. | ||
type Builder struct { | ||
Package *types.Package | ||
|
||
genTypes []*types.Named | ||
comments twtypes.Comments | ||
genTypes []*types.Named | ||
comments twtypes.Comments | ||
validationRules string | ||
} | ||
|
||
// NewBuilder returns a new Builder. | ||
|
@@ -59,6 +62,7 @@ func (g *Builder) Build(cfg *config.Resource) (Generated, error) { | |
Comments: g.comments, | ||
ForProviderType: fp, | ||
AtProviderType: ap, | ||
ValidationRules: g.validationRules, | ||
}, errors.Wrapf(err, "cannot build the Types") | ||
} | ||
|
||
|
@@ -127,6 +131,11 @@ func (g *Builder) AddToBuilder(typeNames *TypeNames, r *resource) (*types.Named, | |
obsType := types.NewNamed(typeNames.ObservationTypeName, types.NewStruct(r.obsFields, r.obsTags), nil) | ||
g.genTypes = append(g.genTypes, obsType) | ||
|
||
for _, p := range r.topLevelRequiredParams { | ||
g.validationRules += "\n" | ||
g.validationRules += fmt.Sprintf(`// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'ObserveOnly' || has(self.forProvider.%s)",message="%s is a required parameter"`, p, p) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We will need to bump the crossplane-runtime dependency so that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just tried this out and we get the following error during CRD creation:
We will need to ensure that we are updating crossplane-runtime and crossplane-tools dependency with management policy, as part of rolling out this feature to the providers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for giving this a try @turkenh.
turkenh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
return paramType, obsType | ||
} | ||
|
||
|
@@ -260,19 +269,33 @@ func NewTypeNames(fieldPaths []string, pkg *types.Package) (*TypeNames, error) { | |
type resource struct { | ||
paramFields, obsFields []*types.Var | ||
paramTags, obsTags []string | ||
topLevelRequiredParams []string | ||
} | ||
|
||
func (r *resource) addParameterField(f *Field, field *types.Var) { | ||
if f.Schema.Optional { | ||
req := !f.Schema.Optional | ||
// Note(turkenh): We are collecting the top level required parameters that | ||
// are not identifier fields. This is for generating CEL validation rules for | ||
// those parameters and not to require them if the management policy is set | ||
// Observe Only. In other words, if we are not creating or managing the | ||
// resource, we don't need to provide those parameters which are: | ||
// - req => required | ||
// - !f.Identifier => not identifiers - i.e. region, zone, etc. | ||
// - len(f.CanonicalPaths) == 1 => top level, i.e. not a nested field | ||
if req && !f.Identifier && len(f.CanonicalPaths) == 1 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Let's assume There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you noticed, we don't support nested identifier fields right now and I added a todo for the future. For now, I believe this should be ok due to the following two reasons:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. As discussed offline, our plan is to make manual external-name configurations via |
||
req = false | ||
r.topLevelRequiredParams = append(r.topLevelRequiredParams, f.TransformedName) | ||
} | ||
|
||
f.Comment.Required = &req | ||
if !req { | ||
r.paramTags = append(r.paramTags, fmt.Sprintf(`json:"%s" tf:"%s"`, f.JSONTag, f.TFTag)) | ||
} else { | ||
// Required fields should not have omitempty tag in json tag. | ||
// TODO(muvaf): This overrides user intent if they provided custom | ||
// JSON tag. | ||
r.paramTags = append(r.paramTags, fmt.Sprintf(`json:"%s" tf:"%s"`, strings.TrimSuffix(f.JSONTag, ",omitempty"), f.TFTag)) | ||
} | ||
req := !f.Schema.Optional | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My understanding is that CRD validation rules have graduated to beta with Kubernetes 1.25. We may not be enforcing required fields at the Kubernetes API level for the previous Kubernetes versions if the corresponding alpha feature is not explicitly enabled. Also, though practically less important because we will making previously required fields optional, we will also be introducing breaking API changes for such clusters. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Correct. This was already discussed during the design here, and there was an agreement that this is reasonable given the reconciler will report the error from cloud API indicating that a required field is missing after the first reconciliation. |
||
f.Comment.Required = &req | ||
r.paramFields = append(r.paramFields, field) | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Edge cases may arise when we use arbitrary templating expressions apart from
{{ .parameters.<argument name> }}
as a parameter toconfig.TemplatedStringAsIdentifier
. This is not common but is theoretically possible. Please also see the comment forconfig.ExternalName.IdentifierFields
, which is also related to the discussion here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I am getting this. Could you elaborate with some examples?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have already discussed these offline but just taking a note here for future reference. Some examples we've found with @sergenyalcin are:
(We have already agreed these are not blockers. Just taking a note here for future reference).