Skip to content
Merged
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
56 changes: 56 additions & 0 deletions manifests/manifests_pools.go
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) {
Copy link
Copy Markdown
Member

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 😄

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

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).

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.

return true
}
}
return false
}
51 changes: 51 additions & 0 deletions pkg/controller/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
G, G, U - basically same as above
G, G - we want G
G, U, U - we just take the last match, based on however the slice got sorted (I guess it's maybe based on manifest name?)
U, U - same scenario

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

G, U, U - we just take the last match, based on however the slice got sorted (I guess it's maybe based on manifest name?)

Yes, the order we had, and still have, in this case is the based on the file name.

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?

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.

Or does the manifest parsing reject the duplicate elsewhere

There's no other filtering. This new function is all we have as filter. The installer doesn't filter either.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

we think both of them to be the generated case,

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,
Expand Down
47 changes: 34 additions & 13 deletions pkg/operator/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"time"

configclientscheme "github.com/openshift/client-go/config/clientset/versioned/scheme"
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
"github.com/openshift/machine-config-operator/manifests"
"github.com/openshift/machine-config-operator/pkg/osimagestream"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
Expand All @@ -35,6 +37,7 @@ import (
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"

"github.com/openshift/api/annotations"
configv1 "github.com/openshift/api/config/v1"
Expand Down Expand Up @@ -715,23 +718,41 @@ func getIgnitionHost(infraStatus *configv1.InfrastructureStatus) (string, error)
}

func (optr *Operator) syncMachineConfigPools(config *renderConfig, _ *configv1.ClusterOperator) error {
mcps := []string{
"manifests/master.machineconfigpool.yaml",
"manifests/worker.machineconfigpool.yaml",
generatedPools, err := manifests.GetMachineConfigPools()
if err != nil {
return fmt.Errorf("failed to get generated MCPs: %w", err)
}
for _, mcp := range generatedPools {
if config.Infra.Status.ControlPlaneTopology != configv1.HighlyAvailableArbiterMode && mcp.Name == ctrlcommon.MachineConfigPoolArbiter {
continue
}

if config.Infra.Status.ControlPlaneTopology == configv1.HighlyAvailableArbiterMode {
mcps = append(mcps, "manifests/arbiter.machineconfigpool.yaml")
}
existing, err := optr.client.MachineconfigurationV1().MachineConfigPools().Get(context.TODO(), mcp.GetName(), metav1.GetOptions{})
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to get MachineConfigPool %s: %w", mcp.GetName(), err)
}

for _, mcp := range mcps {
mcpBytes, err := renderAsset(config, mcp)
if err != nil {
return err
// If the MCP doesn't exist, create it
if apierrors.IsNotFound(err) {
klog.Infof("Creating managed MachineConfigPool %s as it does not exist", mcp.GetName())
if _, err := optr.client.MachineconfigurationV1().MachineConfigPools().Create(context.TODO(), mcp, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("failed to create MachineConfigPool %s: %w", mcp.GetName(), err)
}
continue
}
p := mcoResourceRead.ReadMachineConfigPoolV1OrDie(mcpBytes)
_, _, err = mcoResourceApply.ApplyMachineConfigPool(optr.client.MachineconfigurationV1(), p)
if err != nil {

// Ensure the metadata we manage and want to enforce is present
// We don't care about the spec, the user is free to touch parameters
// in the spec, and we don't want to interfere with them. Merge strategies
// won't work in the spec as it won't allow a user to reset a field to blank
modified := ptr.To(false)
resourcemerge.EnsureObjectMeta(modified, &existing.ObjectMeta, mcp.ObjectMeta)
if !*modified {
continue
}

// The metadata has changed, apply it
if _, err := optr.client.MachineconfigurationV1().MachineConfigPools().Update(context.TODO(), existing, metav1.UpdateOptions{}); err != nil {
return err
}
}
Expand Down