Repository: kubeclipper/kubeclipper
Pull request #398 (2022-11-16 00:57:23)

fix plugin cri registry
Signed-off-by: lyp256 <lyp256@qq.com>

<!-- Thanks for sending a pull request! Here are some tips for you:

1. If you want **faster** PR reviews, read how: https://github.com/kubeclipper/community/blob/master/developer-guide/development/the-pr-author-guide-to-getting-through-code-review.md
2. In case you want to know how your PR got reviewed, read: https://github.com/kubeclipper/community/blob/master/developer-guide/development/code-review-guide.md
3. Here are some coding convetions followed by Kubeclipper community: https://github.com/kubeclipper/community/blob/master/developer-guide/development/coding-conventions.md
-->

### What type of PR is this?
/kind bug
<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->


### What this PR does / why we need it:

### Which issue(s) this PR fixes:
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

### Special notes for reviewers:
```
```

### Does this PR introduced a user-facing change?
<!--
If no, just write "None" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md
-->
```release-note
None
```

### Additional documentation, usage docs, etc.:
<!--
This section can be blank if this pull request does not require a release note.
Please use the following format for linking documentation or pass the
section below:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```

kubeclipper-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeclipper/kubeclipper/pull/398#" title="Author self-approved">lyp256</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **x893675** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @x893675` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubeclipper%2Fkubeclipper).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubeclipper/kubeclipper/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["x893675"]} -->
codecov-commenter: # [Codecov](https://codecov.io/gh/kubeclipper/kubeclipper/pull/398?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) Report
> Merging [#398](https://codecov.io/gh/kubeclipper/kubeclipper/pull/398?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) (e2a47ee) into [master](https://codecov.io/gh/kubeclipper/kubeclipper/commit/d5ad279657d57fe9077b1b48ba393880fd74c665?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) (d5ad279) will **decrease** coverage by `0.05%`.
> The diff coverage is `1.29%`.

[![Impacted file tree graph](https://codecov.io/gh/kubeclipper/kubeclipper/pull/398/graphs/tree.svg?width=650&height=150&src=pr&token=3PWFG2MHTE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper)](https://codecov.io/gh/kubeclipper/kubeclipper/pull/398?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper)

```diff
@@            Coverage Diff             @@
##           master     #398      +/-   ##
==========================================
- Coverage   12.23%   12.17%   -0.06%     
==========================================
  Files         102      103       +1     
  Lines       16890    16969      +79     
==========================================
  Hits         2066     2066              
- Misses      14577    14656      +79     
  Partials      247      247              
```


| [Impacted Files](https://codecov.io/gh/kubeclipper/kubeclipper/pull/398?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper) | Coverage Δ | |
|---|---|---|
| [pkg/apis/core/v1/cri\_utils.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2FwaXMvY29yZS92MS9jcmlfdXRpbHMuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/apis/core/v1/handler.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2FwaXMvY29yZS92MS9oYW5kbGVyLmdv) | `0.33% <0.00%> (+<0.01%)` | :arrow_up: |
| [pkg/apis/core/v1/utils.go](https://codecov.io/gh/kubeclipper/kubeclipper/pull/398/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubeclipper#diff-cGtnL2FwaXMvY29yZS92MS91dGlscy5nbw==) | `54.24% <100.00%> (ø)` | |


Patch:
diff --git a/pkg/apis/core/v1/cri_utils.go b/pkg/apis/core/v1/cri_utils.go
new file mode 100644
index 0000000..4e06cc0
--- /dev/null
+++ b/pkg/apis/core/v1/cri_utils.go
@@ -0,0 +1,179 @@
+// The code in this file is copied to the cluster controller,
+// which should be deleted after reconstruction
+
+package v1
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"sort"
+	"strings"
+	"time"
+
+	apimachineryErrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/kubeclipper/kubeclipper/pkg/component"
+	"github.com/kubeclipper/kubeclipper/pkg/query"
+	"github.com/kubeclipper/kubeclipper/pkg/scheme/common"
+	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1/cri"
+)
+
+// TODO: copy from ClusterController, Remove after refactoring
+func (h *handler) getClusterCRIRegistries(ctx context.Context, c *v1.Cluster) ([]v1.RegistrySpec, error) {
+	type mirror struct {
+		ImageRepoMirror string `json:"imageRepoMirror"`
+	}
+	insecureRegistry := append([]string{}, c.ContainerRuntime.InsecureRegistry...)
+	sort.Strings(insecureRegistry)
+	// add addons mirror registry
+	for _, a := range c.Addons {
+		var m mirror
+		err := json.Unmarshal(a.Config.Raw, &m)
+		if err != nil {
+			continue
+		}
+		if m.ImageRepoMirror != "" {
+			idx, ok := sort.Find(len(insecureRegistry), func(i int) int {
+				return strings.Compare(m.ImageRepoMirror, insecureRegistry[i])
+			})
+			if !ok {
+				if idx == len(insecureRegistry) {
+					insecureRegistry = append(insecureRegistry, m.ImageRepoMirror)
+				} else {
+					insecureRegistry = append(insecureRegistry[:idx+1], insecureRegistry[idx:]...)
+					insecureRegistry[idx] = m.ImageRepoMirror
+				}
+			}
+		}
+	}
+
+	registries := make([]v1.RegistrySpec, 0, len(insecureRegistry)*2+len(c.ContainerRuntime.Registries))
+	// insecure registry
+	for _, host := range insecureRegistry {
+		registries = appendUniqueRegistry(registries,
+			v1.RegistrySpec{Scheme: "http", Host: host},
+			v1.RegistrySpec{Scheme: "https", Host: host, SkipVerify: true})
+	}
+
+	validRegistries := c.ContainerRuntime.Registries[:0]
+	for _, reg := range c.ContainerRuntime.Registries {
+		if reg.RegistryRef == nil || *reg.RegistryRef == "" {
+			// fix reg.RegistryRef=""
+			reg.RegistryRef = nil
+			registries = appendUniqueRegistry(registries,
+				v1.RegistrySpec{Scheme: "http", Host: reg.InsecureRegistry},
+				v1.RegistrySpec{Scheme: "https", Host: reg.InsecureRegistry, SkipVerify: true})
+			validRegistries = append(validRegistries, reg)
+			continue
+		}
+		registry, err := h.clusterOperator.GetRegistry(ctx, *reg.RegistryRef)
+		if err != nil {
+			if apimachineryErrors.IsNotFound(err) {
+				continue
+			}
+			return nil, fmt.Errorf("get registry %s:%w", *reg.RegistryRef, err)
+		}
+		registries = appendUniqueRegistry(registries, registry.RegistrySpec)
+		validRegistries = append(validRegistries, reg)
+	}
+	c.ContainerRuntime.Registries = validRegistries
+	return registries, nil
+}
+
+// TODO: copy from ClusterController, Remove after refactoring
+func appendUniqueRegistry(s []v1.RegistrySpec, items ...v1.RegistrySpec) []v1.RegistrySpec {
+	for _, r := range items {
+		key := r.Scheme + r.Host
+		idx, ok := sort.Find(len(s), func(i int) int {
+			return strings.Compare(key, s[i].Scheme+s[i].Host)
+		})
+		if !ok {
+			if idx == len(s) {
+				s = append(s, r)
+			} else {
+				s = append(s[:idx+1], s[idx:]...)
+				s[idx] = r
+			}
+		}
+	}
+	return s
+}
+
+func registriesEqual(a, b []v1.RegistrySpec) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if a[i] != b[i] {
+			return false
+		}
+	}
+	return true
+}
+
+func (h *handler) getCRIRegistriesStep(ctx context.Context, cluster *v1.Cluster, registries []v1.RegistrySpec) (*v1.Step, error) {
+	if !registriesEqual(cluster.Status.Registries, registries) {
+		q := query.New()
+		q.LabelSelector = fmt.Sprintf("%s=%s", common.LabelClusterName, cluster.Name)
+
+		nodeList, err := h.clusterOperator.ListNodes(ctx, q)
+		if err != nil {
+			return nil, fmt.Errorf("list")
+		}
+		return criRegistryUpdateStep(cluster, registries, nodeList.Items)
+	}
+	return nil, nil
+}
+
+func criRegistryUpdateStep(cluster *v1.Cluster, registries []v1.RegistrySpec, nodes []v1.Node) (*v1.Step, error) {
+	var (
+		step     component.StepRunnable
+		identity string
+	)
+	switch cluster.ContainerRuntime.Type {
+	case v1.CRIDocker:
+		identity = cri.DockerInsecureRegistryConfigureIdentity
+		step = &cri.DockerInsecureRegistryConfigure{
+			InsecureRegistry: cri.ToDockerInsecureRegistry(registries),
+		}
+	case v1.CRIContainerd:
+		identity = cri.ContainerdRegistryConfigureIdentity
+		step = &cri.ContainerdRegistryConfigure{
+			Registries: cri.ToContainerdRegistryConfig(registries),
+			// TODO: get from config
+			ConfigDir: cri.ContainerdDefaultRegistryConfigDir,
+		}
+	default:
+		return nil, fmt.Errorf("unknown CRI type:%s", cluster.ContainerRuntime.Type)
+	}
+	stepData, err := json.Marshal(step)
+	if err != nil {
+		return nil, fmt.Errorf("step marshal:%w", err)
+	}
+	var allNodes []v1.StepNode
+	for _, node := range nodes {
+		allNodes = append(allNodes, v1.StepNode{
+			ID:       node.Name,
+			IPv4:     node.Status.Ipv4DefaultIP,
+			Hostname: node.Labels[common.LabelHostname],
+		})
+	}
+	return &v1.Step{
+		Name:   "update-cri-registry-config",
+		Nodes:  allNodes,
+		Action: v1.ActionInstall,
+		Timeout: metav1.Duration{
+			Duration: time.Second * 30,
+		},
+		Commands: []v1.Command{
+			{
+				Type:          v1.CommandCustom,
+				Identity:      identity,
+				CustomCommand: stepData,
+			},
+		},
+	}, nil
+}
diff --git a/pkg/apis/core/v1/handler.go b/pkg/apis/core/v1/handler.go
index c5196b6..42faa78 100644
--- a/pkg/apis/core/v1/handler.go
+++ b/pkg/apis/core/v1/handler.go
@@ -27,7 +27,6 @@ import (
 	"math/rand"
 	"net/http"
 	"path/filepath"
-	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -591,87 +590,6 @@ func (h *handler) checkNode(ctx context.Context, project string, nodeIDs []strin
 	return nil
 }
 
-// TODO: copy from ClusterController, Remove after refactoring
-func (h *handler) getClusterCRIRegistries(ctx context.Context, c *v1.Cluster) ([]v1.RegistrySpec, error) {
-	type mirror struct {
-		ImageRepoMirror string `json:"imageRepoMirror"`
-	}
-	insecureRegistry := append([]string{}, c.ContainerRuntime.InsecureRegistry...)
-	sort.Strings(insecureRegistry)
-	// add addons mirror registry
-	for _, a := range c.Addons {
-		var m mirror
-		err := json.Unmarshal(a.Config.Raw, &m)
-		if err != nil {
-			continue
-		}
-		if m.ImageRepoMirror != "" {
-			idx, ok := sort.Find(len(insecureRegistry), func(i int) int {
-				return strings.Compare(m.ImageRepoMirror, insecureRegistry[i])
-			})
-			if !ok {
-				if idx == len(insecureRegistry) {
-					insecureRegistry = append(insecureRegistry, m.ImageRepoMirror)
-				} else {
-					insecureRegistry = append(insecureRegistry[:idx+1], insecureRegistry[idx:]...)
-					insecureRegistry[idx] = m.ImageRepoMirror
-				}
-			}
-		}
-	}
-
-	registries := make([]v1.RegistrySpec, 0, len(insecureRegistry)*2+len(c.ContainerRuntime.Registries))
-	// insecure registry
-	for _, host := range insecureRegistry {
-		registries = appendUniqueRegistry(registries,
-			v1.RegistrySpec{Scheme: "http", Host: host},
-			v1.RegistrySpec{Scheme: "https", Host: host, SkipVerify: true})
-	}
-
-	validRegistries := c.ContainerRuntime.Registries[:0]
-	for _, reg := range c.ContainerRuntime.Registries {
-		if reg.RegistryRef == nil || *reg.RegistryRef == "" {
-			// fix reg.RegistryRef=""
-			reg.RegistryRef = nil
-			registries = appendUniqueRegistry(registries,
-				v1.RegistrySpec{Scheme: "http", Host: reg.InsecureRegistry},
-				v1.RegistrySpec{Scheme: "https", Host: reg.InsecureRegistry, SkipVerify: true})
-			validRegistries = append(validRegistries, reg)
-			continue
-		}
-		registry, err := h.clusterOperator.GetRegistry(ctx, *reg.RegistryRef)
-		if err != nil {
-			if apimachineryErrors.IsNotFound(err) {
-				continue
-			}
-			return nil, fmt.Errorf("get registry %s:%w", *reg.RegistryRef, err)
-		}
-		registries = appendUniqueRegistry(registries, registry.RegistrySpec)
-		validRegistries = append(validRegistries, reg)
-	}
-	c.ContainerRuntime.Registries = validRegistries
-	return registries, nil
-}
-
-// TODO: copy from ClusterController, Remove after refactoring
-func appendUniqueRegistry(s []v1.RegistrySpec, items ...v1.RegistrySpec) []v1.RegistrySpec {
-	for _, r := range items {
-		key := r.Scheme + r.Host
-		idx, ok := sort.Find(len(s), func(i int) int {
-			return strings.Compare(key, s[i].Scheme+s[i].Host)
-		})
-		if !ok {
-			if idx == len(s) {
-				s = append(s, r)
-			} else {
-				s = append(s[:idx+1], s[idx:]...)
-				s[idx] = r
-			}
-		}
-	}
-	return s
-}
-
 func (h *handler) UpdateClusters(request *restful.Request, response *restful.Response) {
 	name := request.PathParameter("name")
 	c := v1.Cluster{}
@@ -2217,7 +2135,7 @@ func (h *handler) InstallOrUninstallPlugins(request *restful.Request, response *
 		action = v1.ActionUninstall
 		operationAction = v1.OperationUninstallComponents
 	}
-	op, err := h.parseOperationFromComponent(extraMeta, pcs.Addons, clu, action)
+	op, err := h.parseOperationFromComponent(ctx, extraMeta, pcs.Addons, clu, action)
 	if err != nil {
 		restplus.HandleInternalError(response, request, err)
 		return
@@ -2227,23 +2145,42 @@ func (h *handler) InstallOrUninstallPlugins(request *restful.Request, response *
 		restplus.HandleBadRequest(response, request, errors.New("the current operation steps is empty and cannot be performed"))
 		return
 	}
+
+	op.Labels[common.LabelTimeoutSeconds] = timeoutSecs
+	op.Labels[common.LabelOperationAction] = operationAction
+	op.Status.Status = v1.OperationStatusRunning
+
 	if !dryRun {
+		// cluster.Status.Registries will be updated
+		var statusRegistry []v1.RegistrySpec
+		statusRegistry, err = h.getClusterCRIRegistries(ctx, clu)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+		criStep, err := h.getCRIRegistriesStep(ctx, clu, statusRegistry)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+		if criStep != nil {
+			op.Steps = append(op.Steps[:1], op.Steps...)
+			op.Steps[0] = *criStep
+			clu.Status.Registries = statusRegistry
+		}
 		clu.Status.Phase = v1.ClusterUpdating
 		_, err = h.clusterOperator.UpdateCluster(context.TODO(), clu)
 		if err != nil {
 			restplus.HandleInternalError(response, request, err)
 			return
 		}
-	}
-	op.Labels[common.LabelTimeoutSeconds] = timeoutSecs
-	op.Labels[common.LabelOperationAction] = operationAction
-	op.Status.Status = v1.OperationStatusRunning
-	if !dryRun {
+
 		op, err = h.opOperator.CreateOperation(context.TODO(), op)
 		if err != nil {
 			restplus.HandleInternalError(response, request, err)
 			return
 		}
+
 	}
 	go func(o *v1.Operation, opts *service.Options, oPcs *PatchComponents) {
 		if err := h.delivery.DeliverTaskOperation(context.TODO(), o, opts); err != nil {
@@ -2263,11 +2200,6 @@ func (h *handler) InstallOrUninstallPlugins(request *restful.Request, response *
 				logger.Error("add or remove component from cluster", zap.Error(err))
 				return
 			}
-			newCluster.Status.Registries, err = h.getClusterCRIRegistries(request.Request.Context(), newCluster)
-			if err != nil {
-				logger.Error("update cri registry failed", zap.Error(err))
-				return
-			}
 			_, err = h.clusterOperator.UpdateCluster(context.TODO(), newCluster)
 			if err != nil {
 				logger.Error("update cluster metadata error", zap.Error(err))
diff --git a/pkg/apis/core/v1/utils.go b/pkg/apis/core/v1/utils.go
index af2824d..f5cf999 100644
--- a/pkg/apis/core/v1/utils.go
+++ b/pkg/apis/core/v1/utils.go
@@ -30,7 +30,6 @@ import (
 	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
 
 	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
-
 	"github.com/kubeclipper/kubeclipper/pkg/query"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component"
@@ -257,7 +256,7 @@ func getRecoveryStep(c *v1.Cluster, bp *v1.BackupPoint, b *v1.Backup, restoreDir
 }
 
 // parseOperationFromComponent parse operation instance from component
-func (h *handler) parseOperationFromComponent(extraMetadata *component.ExtraMetadata, addons []v1.Addon, c *v1.Cluster, action v1.StepAction) (*v1.Operation, error) {
+func (h *handler) parseOperationFromComponent(ctx context.Context, extraMetadata *component.ExtraMetadata, addons []v1.Addon, c *v1.Cluster, action v1.StepAction) (*v1.Operation, error) {
 	var err error
 	op := &v1.Operation{}
 	op.Name = uuid.New().String()
@@ -266,7 +265,7 @@ func (h *handler) parseOperationFromComponent(extraMetadata *component.ExtraMeta
 		common.LabelTopologyRegion: extraMetadata.Masters[0].Region,
 	}
 	// with extra meta data
-	ctx := component.WithExtraMetadata(context.TODO(), *extraMetadata)
+	ctx = component.WithExtraMetadata(ctx, *extraMetadata)
 	op.Steps, err = h.parseAddonStep(ctx, c, addons, action)
 	if err != nil {
 		return nil, err
diff --git a/pkg/cli/create/create_cluster.go b/pkg/cli/create/create_cluster.go
index b54d67f..5bce697 100644
--- a/pkg/cli/create/create_cluster.go
+++ b/pkg/cli/create/create_cluster.go
@@ -53,16 +53,16 @@ const (
   Create cluster using command line`
 	createClusterExample = `
   # Create cluster offline. The default value of offline is true, so it can be omitted.
-  kcctl create cluster --name demo --master 192.168.10.123
+  kcctl create cluster --name demo --master 192.168.10.123 --project pro-demo
 
   # Create cluster online
-  kcctl create cluster --name demo --master 192.168.10.123 --offline false --local-registry 192.168.10.123:5000
+  kcctl create cluster --name demo --master 192.168.10.123 --offline false --local-registry 192.168.10.123:5000 --project pro-demo
 
   # Create cluster with taint manage
-  kcctl create cluster --name demo --master 192.168.10.123 --untaint-master
+  kcctl create cluster --name demo --master 192.168.10.123 --untaint-master --project pro-demo
 
   # Create cluster with worker.
-  kcctl create cluster --name demo --master 192.168.10.123 --worker 192.168.10.124
+  kcctl create cluster --name demo --master 192.168.10.123 --worker 192.168.10.124 --project pro-demo
 
   Please read 'kcctl create cluster -h' get more create cluster flags.`
 )
@@ -84,6 +84,7 @@ type CreateClusterOptions struct {
 	CertSans      []string
 	CaCertFile    string
 	CaKeyFile     string
+	Project       string
 }
 
 var (
@@ -136,6 +137,7 @@ func NewCmdCreateCluster(streams options.IOStreams) *cobra.Command {
 	cmd.Flags().StringSliceVar(&o.CertSans, "cert-sans", o.CertSans, "k8s cluster certificate signing ipList or domainList")
 	cmd.Flags().StringVar(&o.CaCertFile, "ca-cert", o.CaCertFile, "k8s external root-ca cert file")
 	cmd.Flags().StringVar(&o.CaKeyFile, "ca-key", o.CaKeyFile, "k8s external root-ca key file")
+	cmd.Flags().StringVar(&o.Project, "project", o.Project, "the project to which this k8s cluster belongs")
 	o.CliOpts.AddFlags(cmd.Flags())
 	o.PrintFlags.AddFlags(cmd)
 
@@ -366,6 +368,10 @@ func (l *CreateClusterOptions) newCluster() *v1.Cluster {
 		Status: v1.ClusterStatus{},
 	}
 
+	if l.Project != "" {
+		c.Labels = map[string]string{common.LabelProject: l.Project}
+	}
+
 	masters := make([]v1.WorkerNode, 0)
 	for _, n := range l.Masters {
 		if l.UntaintMaster {


Test patch:
diff --git a/pkg/apis/core/v1/utils_test.go b/pkg/apis/core/v1/utils_test.go
index 33c7e2d..6c24c5f 100644
--- a/pkg/apis/core/v1/utils_test.go
+++ b/pkg/apis/core/v1/utils_test.go
@@ -19,6 +19,7 @@
 package v1
 
 import (
+	"context"
 	"encoding/json"
 	"testing"
 
@@ -241,7 +242,7 @@ func Test_parseOperationFromComponent(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			_, err := h.parseOperationFromComponent(test.arg.meta, test.arg.components, test.arg.cluster, test.arg.action)
+			_, err := h.parseOperationFromComponent(context.Background(), test.arg.meta, test.arg.components, test.arg.cluster, test.arg.action)
 			if err != nil {
 				t.Errorf("  parseOperationFromComponent() error: %v", err)
 			}