-
Notifications
You must be signed in to change notification settings - Fork 488
MCO-2032: Avoid MCPs duplication at install time #5518
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
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 |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| package manifests | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "reflect" | ||
|
|
||
| mcfgv1 "github.com/openshift/api/machineconfiguration/v1" | ||
| "github.com/openshift/machine-config-operator/lib/resourceread" | ||
| ) | ||
|
|
||
| const ( | ||
| ManifestFileNameMCPMaster ManifestMachineConfigPool = "master.machineconfigpool.yaml" | ||
| ManifestFileNameMCPWorker ManifestMachineConfigPool = "worker.machineconfigpool.yaml" | ||
| ManifestFileNameMCPArbiter ManifestMachineConfigPool = "arbiter.machineconfigpool.yaml" | ||
| ) | ||
|
|
||
| // ManifestMachineConfigPool represents the filename of a built-in MachineConfigPool manifest | ||
| // embedded in the operator. | ||
| type ManifestMachineConfigPool string | ||
|
|
||
| // GetMachineConfigPool reads and parses the given built-in MachineConfigPool manifest. | ||
| func GetMachineConfigPool(pool ManifestMachineConfigPool) (*mcfgv1.MachineConfigPool, error) { | ||
| content, err := ReadFile(string(pool)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error reading pool manifest %s: %v", string(pool), err) | ||
| } | ||
| return resourceread.ReadMachineConfigPoolV1OrDie(content), nil | ||
| } | ||
|
|
||
| // GetMachineConfigPools reads and parses all built-in MachineConfigPool manifests | ||
| // (master, worker, and arbiter) | ||
| func GetMachineConfigPools() ([]*mcfgv1.MachineConfigPool, error) { | ||
| var pools []*mcfgv1.MachineConfigPool | ||
| for _, pool := range []ManifestMachineConfigPool{ManifestFileNameMCPMaster, ManifestFileNameMCPWorker, ManifestFileNameMCPArbiter} { | ||
| poolObj, err := GetMachineConfigPool(pool) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| pools = append(pools, poolObj) | ||
| } | ||
| return pools, nil | ||
| } | ||
|
|
||
| // ContainsMachineConfigPool reports whether pool is identical (via reflect.DeepEqual) to any | ||
| // of the pools in generatedPools. | ||
| func ContainsMachineConfigPool(generatedPools []*mcfgv1.MachineConfigPool, pool *mcfgv1.MachineConfigPool) bool { | ||
| if pool == nil || generatedPools == nil { | ||
| return false | ||
| } | ||
| for _, generated := range generatedPools { | ||
| if reflect.DeepEqual(generated, pool) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import ( | |
| "path/filepath" | ||
| "time" | ||
|
|
||
| "github.com/openshift/machine-config-operator/manifests" | ||
| "github.com/openshift/machine-config-operator/pkg/imageutils" | ||
| "github.com/openshift/machine-config-operator/pkg/osimagestream" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
|
|
@@ -218,6 +219,11 @@ func (b *Bootstrap) Run(destDir string) error { | |
| return fmt.Errorf("error creating feature gates handler: %w", err) | ||
| } | ||
|
|
||
| // Deduplicate pools to avoid generated ones conflict with user overrides | ||
| if pools, err = filterPools(pools); err != nil { | ||
| return fmt.Errorf("error filtering pools: %w", err) | ||
| } | ||
|
|
||
| var osImageStream *mcfgv1alpha1.OSImageStream | ||
| // Enable OSImageStreams if the FeatureGate is active and the deployment is not OKD | ||
| if osimagestream.IsFeatureEnabled(fgHandler) { | ||
|
|
@@ -403,6 +409,51 @@ func (b *Bootstrap) Run(destDir string) error { | |
|
|
||
| } | ||
|
|
||
| // filterPools deduplicates MachineConfigPools by name. When multiple pools share the same | ||
| // name, user-provided pools (those not identical to the operator's built-in manifests) are | ||
| // preferred over automatically generated ones. | ||
| func filterPools(pools []*mcfgv1.MachineConfigPool) ([]*mcfgv1.MachineConfigPool, error) { | ||
| groupedPools := make(map[string][]*mcfgv1.MachineConfigPool) | ||
|
|
||
| // Group the pools by name | ||
| for _, pool := range pools { | ||
| groupedPools[pool.Name] = append(groupedPools[pool.Name], pool) | ||
| } | ||
|
|
||
| generatedPools, err := manifests.GetMachineConfigPools() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get generated MCPs: %w", err) | ||
| } | ||
|
|
||
| // Now ensure we have only one pool, no duplicates | ||
| // Discard generated ones if required | ||
| var result []*mcfgv1.MachineConfigPool | ||
| for _, poolObjs := range groupedPools { | ||
| if len(poolObjs) == 1 { | ||
| // Nothing to do, it's already unique | ||
| result = append(result, poolObjs[0]) | ||
| continue | ||
| } | ||
|
|
||
| // Remove generated ones first | ||
| var lastPool *mcfgv1.MachineConfigPool | ||
| for _, poolObj := range poolObjs { | ||
| if manifests.ContainsMachineConfigPool(generatedPools, poolObj) { | ||
| // Handle a dev error in case the same pool is | ||
|
Contributor
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. This is interesting. Basically, there's these scenarios you're accounting for here: (G = generated, U = user provided, assuming same pool name) G, U - this is the easiest, we want U And all of them are "valid". I want to say anything except scenario 1 (G, U) should actually be a failure, because either the user somehow provided conflicting manifests, or we somehow generated duplicates, both of which should be... fails? Would U, U or G, U, U work in the cluster? Or does the manifest parsing reject the duplicate elsewhere?
Contributor
Author
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.
Yes, the order we had, and still have, in this case is the based on the file name.
And I would agree, but this exact scenario is how jobs are now configured... https://github.com/openshift/release/pull/73173/changes#diff-4dfd4f4e866cda920dd45858843286a4a93ae1318705fc5b0e5bdc66842afdbfR11 (forget about the manifest_ prefix, the prow job does some mangling with the name and the prefix is removed before the manifest is copied to the installer manifests). I wouldn't like to break the jobs we already have.
There's no other filtering. This new function is all we have as filter. The installer doesn't filter either.
Contributor
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. Hmm, ok, so the installer is doing G, G instead of G, U. Weird, but I guess there's no harm in us filtering for the additional configs for now.
Contributor
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 a random followup thought, it this always deterministic? I ask because I wonder if in the case of (default manifest is rhel9, installer provides rhel10) we think both of them to be the generated case, and we take the "last object in the list" but what's guarantee'ing the last object to be then rhel10 overwrite one?
Contributor
Author
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. @yuqi-zhang So, the default manifest doesn't set the stream at all, we leave it blank and let the render controller pick the default stream when rendering the MCs. So, in theory, that case shouldn't happen. Anyways, the order of the files is deterministic and it's based on the filename.
That shouldn't be possible, as the deepequals should properly identify the generated one (the rhel-9 one) and the rhel-10 one should, given that, handled always as user provided. |
||
| // generated more than once. In that case, keep at | ||
| // least one generated pool | ||
| if lastPool == nil { | ||
| lastPool = poolObj | ||
| } | ||
| continue | ||
| } | ||
| lastPool = poolObj | ||
| } | ||
| result = append(result, lastPool) | ||
| } | ||
| return result, nil | ||
| } | ||
|
|
||
| func (b *Bootstrap) fetchOSImageStream( | ||
| imageStream *imagev1.ImageStream, | ||
| cconfig *mcfgv1.ControllerConfig, | ||
|
|
||
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.
question (non-blocking): Why a DeepEqual() here? Is going by name / UUID insufficient to determine uniqueness here?
I'm most likely missing some greater context 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.
Based on how this is used, I think Pablo is using this to check for the exact generated MCP from our manifests, if we find two pools of the same name (i.e. a user provided an MCP override with the same name).
This does account for all metadata, including timestamps, etc. which... may cause unintended false negatives, but I think since this is before any controller/api modification of the MCP objects, in practice that shouldn't happen, so probably fine as is.
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.
Correct, and it "works" only because we control the generated data and we know there's nothing dynamic on it. It won't work with MCPs that have been already applied and have, as you said, timestamps, generation, etc.