-
Notifications
You must be signed in to change notification settings - Fork 63
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
Data dir rfc #410
Data dir rfc #410
Changes from 7 commits
2b0fe9e
d42b9aa
007ef5a
0875ab7
6a630a2
b1f26f2
ac1086b
bb7c01e
71bde81
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 |
---|---|---|
@@ -1,10 +1,23 @@ | ||
## Validation Checks | ||
|
||
### On Update | ||
|
||
#### Data Directories | ||
|
||
Prevent the creation of new objects with an env var (under `spec.agentEnvVars`) with a name of `CATTLE_AGENT_VAR_DIR`. | ||
On update, also prevent new env vars with this name from being added but allow them to be removed. Rancher will perform | ||
a one-time migration to move the system-agent data dir definition to the top level field from the `AgentEnvVars` | ||
section. A secondary validator will ensure that the effective data directory for the `system-agent` is not different | ||
from the one chosen during cluster creation. Additionally, the changing of a data directory for the `system-agent`, | ||
kubernetes distro (RKE2/K3s), and CAPR components is also prohibited. | ||
|
||
## Mutation Checks | ||
|
||
### On Update | ||
|
||
#### Dynamic Schema Drop | ||
|
||
Check for the presence of the `provisioning.cattle.io/allow-dynamic-schema-drop` annotation. If the value is `"true"`, | ||
perform no mutations. If the value is not present or not `"true"`, compare the value of the `dynamicSchemaSpec` field | ||
for each `machinePool`, to its' previous value. If the values are not identical, revert the value for the | ||
Check for the presence of the `provisioning.cattle.io/allow-dynamic-schema-drop` annotation. If the value is `"true"`, | ||
perform no mutations. If the value is not present or not `"true"`, compare the value of the `dynamicSchemaSpec` field | ||
for each `machinePool`, to its' previous value. If the values are not identical, revert the value for the | ||
`dynamicSchemaSpec` for the specific `machinePool`, but do not reject the request. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,12 @@ import ( | |
"encoding/base64" | ||
"fmt" | ||
"net/http" | ||
"reflect" | ||
"regexp" | ||
"slices" | ||
|
||
v1 "github.com/rancher/rancher/pkg/apis/provisioning.cattle.io/v1" | ||
rkev1 "github.com/rancher/rancher/pkg/apis/rke.cattle.io/v1" | ||
"github.com/rancher/webhook/pkg/admission" | ||
"github.com/rancher/webhook/pkg/clients" | ||
v3 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3" | ||
|
@@ -26,10 +29,15 @@ import ( | |
"k8s.io/utils/trace" | ||
) | ||
|
||
const globalNamespace = "cattle-global-data" | ||
const ( | ||
globalNamespace = "cattle-global-data" | ||
systemAgentVarDirEnvVar = "CATTLE_AGENT_VAR_DIR" | ||
) | ||
|
||
var mgmtNameRegex = regexp.MustCompile("^c-[a-z0-9]{5}$") | ||
var fleetNameRegex = regexp.MustCompile("^[^-][-a-z0-9]+$") | ||
var ( | ||
mgmtNameRegex = regexp.MustCompile("^c-[a-z0-9]{5}$") | ||
fleetNameRegex = regexp.MustCompile("^[^-][-a-z0-9]+$") | ||
) | ||
|
||
// NewProvisioningClusterValidator returns a new validator for provisioning clusters | ||
func NewProvisioningClusterValidator(client *clients.Clients) *ProvisioningClusterValidator { | ||
|
@@ -78,10 +86,12 @@ type provisioningAdmitter struct { | |
func (p *provisioningAdmitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) { | ||
listTrace := trace.New("provisioningClusterValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username}) | ||
defer listTrace.LogIfLong(admission.SlowTraceDuration) | ||
|
||
oldCluster, cluster, err := objectsv1.ClusterOldAndNewFromRequest(&request.AdmissionRequest) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
response := &admissionv1.AdmissionResponse{} | ||
if request.Operation == admissionv1.Create || request.Operation == admissionv1.Update { | ||
if err := p.validateClusterName(request, response, cluster); err != nil || response.Result != nil { | ||
|
@@ -103,6 +113,14 @@ func (p *provisioningAdmitter) Admit(request *admission.Request) (*admissionv1.A | |
if err := p.validateCloudCredentialAccess(request, response, oldCluster, cluster); err != nil || response.Result != nil { | ||
return response, err | ||
} | ||
|
||
if response = p.validateAgentEnvVars(request, oldCluster, cluster); response.Result != nil { | ||
return response, err | ||
} | ||
|
||
if response = p.validateDataDirectories(request, oldCluster, cluster); response.Result != nil { | ||
return response, err | ||
} | ||
} | ||
|
||
if err := p.validatePSACT(request, response, cluster); err != nil || response.Result != nil { | ||
|
@@ -113,6 +131,83 @@ func (p *provisioningAdmitter) Admit(request *admission.Request) (*admissionv1.A | |
return response, nil | ||
} | ||
|
||
// validateAgentEnvVars will validate the provisioning cluster object's AgentEnvVars, ensuring that the | ||
// "CATTLE_AGENT_VAR_DIR" env var is not allowed to be added to an existing cluster, or set upon create for a new | ||
// cluster. | ||
func (p *provisioningAdmitter) validateAgentEnvVars(request *admission.Request, oldCluster, newCluster *v1.Cluster) *admissionv1.AdmissionResponse { | ||
if newCluster.Spec.RKEConfig == nil { | ||
return admission.ResponseAllowed() | ||
} | ||
if request.Operation == admissionv1.Create { | ||
if slices.ContainsFunc(newCluster.Spec.AgentEnvVars, func(envVar rkev1.EnvVar) bool { | ||
return envVar.Name == systemAgentVarDirEnvVar | ||
}) { | ||
return admission.ResponseBadRequest( | ||
fmt.Sprintf(`"%s" cannot be set within "cluster.Spec.RKEConfig.AgentEnvVars": use "cluster.Spec.RKEConfig.DataDirectories.SystemAgent"`, systemAgentVarDirEnvVar)) | ||
} | ||
return admission.ResponseAllowed() | ||
} | ||
if request.Operation == admissionv1.Update { | ||
var oldCount, newCount int | ||
for _, envVar := range oldCluster.Spec.AgentEnvVars { | ||
if envVar.Name == systemAgentVarDirEnvVar { | ||
oldCount++ | ||
} | ||
} | ||
for _, envVar := range newCluster.Spec.AgentEnvVars { | ||
if envVar.Name == systemAgentVarDirEnvVar { | ||
newCount++ | ||
} | ||
} | ||
if newCount > oldCount { | ||
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 not sure that this works as intended. From what I can see here, I could change this value as long as I added/removed in a single step. For example, if I change That being said, I'm not 100% sure that this is intentional - open to hearing otherwise. 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. Honestly it's not super clear but it is functionally correct: this function is making sure that new
so if we were defining the env var previously, and we haven't changed either the env var or data directory field in this update, assume it's fine (actually as I look at it we shouldn't allow this without validating the other data directories but you get the idea). 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 might be missing something, but the current method will only stop me from adding/removing the env var right? So if I just change it, it's allowed. From your reply we don't want that, it needs to be the same value as before correct? |
||
return admission.ResponseBadRequest( | ||
fmt.Sprintf(`"%s" cannot be set within "cluster.Spec.RKEConfig.AgentEnvVars": use "cluster.Spec.RKEConfig.DataDirectories.SystemAgent"`, systemAgentVarDirEnvVar)) | ||
} | ||
} | ||
|
||
return admission.ResponseAllowed() | ||
} | ||
|
||
// validateDataDirectories will validate updates to the cluster object to ensure the data directories are not changed. | ||
// The only exception when a data directory is allowed to be changed is if cluster.Spec.AgentEnvVars has an env var with | ||
// a name of "CATTLE_AGENT_VAR_DIR", which Rancher will perform a one-time migration to set the | ||
// cluster.Spec.RKEConfig.DataDirectories.SystemAgent field for the cluster. validateAgentEnvVars will ensure | ||
// "CATTLE_AGENT_VAR_DIR" is not added, so this exception only applies to the one-time Rancher migration. | ||
func (p *provisioningAdmitter) validateDataDirectories(request *admission.Request, oldCluster, newCluster *v1.Cluster) *admissionv1.AdmissionResponse { | ||
if request.Operation != admissionv1.Update { | ||
return admission.ResponseAllowed() | ||
} | ||
if newCluster.Spec.RKEConfig == nil { | ||
return admission.ResponseAllowed() | ||
} | ||
oldSystemAgentVarDirEnvVars := slices.DeleteFunc(slices.Clone(oldCluster.Spec.AgentEnvVars), func(envVar rkev1.EnvVar) bool { | ||
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: It occurs to me that you could do this better with a simple extraction func to find the variable. Something like:
Main benefit that we get here is that we are no longer cloning/modifying a slice, so I'd imagine the performance here is going to be a bit better. 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. The issue with this suggestion is that it returns the first occurrence of the env var, and we want the last, as it's possible to specify an env var multiple times. |
||
return envVar.Name != systemAgentVarDirEnvVar | ||
}) | ||
// will be true up until rancher performs one time migration to set SystemAgent data directory if env var was previously set | ||
if len(oldSystemAgentVarDirEnvVars) > 0 { | ||
newSystemAgentVarDirEnvVars := slices.DeleteFunc(slices.Clone(newCluster.Spec.AgentEnvVars), func(envVar rkev1.EnvVar) bool { | ||
return envVar.Name != systemAgentVarDirEnvVar | ||
}) | ||
if !reflect.DeepEqual(newSystemAgentVarDirEnvVars, oldSystemAgentVarDirEnvVars) || oldCluster.Spec.RKEConfig.DataDirectories.SystemAgent != newCluster.Spec.RKEConfig.DataDirectories.SystemAgent { | ||
if newCluster.Spec.RKEConfig.DataDirectories.SystemAgent != oldSystemAgentVarDirEnvVars[len(oldSystemAgentVarDirEnvVars)-1].Value { | ||
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: I think that this is a bit tough to follow. I think this would also be easier to write once the actual env var is extracted and you can move out of slices. In addition, since the second if statement doesn't have an else (or any other additional logic). it can be combined into one if statement. |
||
// only valid during migration | ||
return admission.ResponseBadRequest( | ||
fmt.Sprintf(`"%s" cannot be set within "cluster.Spec.RKEConfig.AgentEnvVars": use "cluster.Spec.RKEConfig.DataDirectories.SystemAgent"`, systemAgentVarDirEnvVar)) | ||
} | ||
} | ||
} else if oldCluster.Spec.RKEConfig.DataDirectories.SystemAgent != newCluster.Spec.RKEConfig.DataDirectories.SystemAgent { | ||
return admission.ResponseBadRequest("SystemAgent data directory cannot be changed after cluster creation") | ||
} | ||
if oldCluster.Spec.RKEConfig.DataDirectories.Provisioning != newCluster.Spec.RKEConfig.DataDirectories.Provisioning { | ||
return admission.ResponseBadRequest("Provisioning data directory cannot be changed after cluster creation") | ||
} | ||
if oldCluster.Spec.RKEConfig.DataDirectories.K8sDistro != newCluster.Spec.RKEConfig.DataDirectories.K8sDistro { | ||
return admission.ResponseBadRequest("Distro data directory cannot be changed after cluster creation") | ||
} | ||
|
||
return admission.ResponseAllowed() | ||
} | ||
|
||
func (p *provisioningAdmitter) validateCloudCredentialAccess(request *admission.Request, response *admissionv1.AdmissionResponse, oldCluster, newCluster *v1.Cluster) error { | ||
if newCluster.Spec.CloudCredentialSecretName == "" || | ||
oldCluster.Spec.CloudCredentialSecretName == newCluster.Spec.CloudCredentialSecretName { | ||
|
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 would handle the "ok" cases differently than this. Right now this depends on
response.Result
being nil forResponseAllowed()
which I think is very dangerous long term. I would recommend one of the following options:response != nil
to see if you should break with a response!response.Allowed
- this way, if it fails we short-circuit and return early.*admissionv1.AdmissionResponse
. Use the bool to check if the response was allowed or not.Big thing here is not to closely couple this code to what the allowed response does like it's currently doing.