Repository: kubernetes-sigs/kwok
Pull request #774 (2023-08-17 04:00:27)

Clean up some code
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

/kind cleanup

#### What this PR does / why we need it:

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
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 your reviewer:

#### Does this PR introduce 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://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
NONE
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

<!--
This section can be blank if this pull request does not require a release note.

When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs
NONE
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kwok/pull/774#" title="Author self-approved">wzshiming</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

- ~~[OWNERS](https://github.com/kubernetes-sigs/kwok/blob/main/OWNERS)~~ [wzshiming]

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":[]} -->
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *k8s-kwok* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 325a9563466f8ad9b28d00e609a33b9af436ed37 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/k8s-kwok/deploys/64dd9b5d76a44100086599c7 |
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | 325a9563466f8ad9b28d00e609a33b9af436ed37 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692023564493918208) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | 325a9563466f8ad9b28d00e609a33b9af436ed37 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692023564493918208) | true | `/test pull-kwok-verify-main`
pull-kwok-unit-test-main | 5499f040a17596cfd3d5445b1130b1839cdfe3f8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-unit-test-main/1692026531737505792) | true | `/test pull-kwok-unit-test-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-unit-test-main | 5499f040a17596cfd3d5445b1130b1839cdfe3f8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-unit-test-main/1692026531737505792) | true | `/test pull-kwok-unit-test-main`
pull-kwok-verify-main | 5499f040a17596cfd3d5445b1130b1839cdfe3f8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692026531636842496) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | 2f5a8a1cc7d349baf4eaf0d3c1362f25f0e070dc | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692027580141539328) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | c4e6a606126d135ccf7379e88224743b16b7a5b4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692054616121806848) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | cb85f66eb087d02d571dd7f4a07717e5cb0d4cc4 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692063751760515072) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | 9511ea55729dda197497d38f71900c8b63f542e0 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692067199201579008) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | 9511ea55729dda197497d38f71900c8b63f542e0 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692067199201579008) | true | `/test pull-kwok-verify-main`
pull-kwok-e2e-test-main | 9511ea55729dda197497d38f71900c8b63f542e0 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-e2e-test-main/1692067199339991040) | true | `/test pull-kwok-e2e-test-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | cc29dc99ba9e93a21a19996fe6d6c9a4e9a8b869 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692076072536903680) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | cc29dc99ba9e93a21a19996fe6d6c9a4e9a8b869 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692087243948167168) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | 010a53ae615b93ba017e6e9e952da1a9d93f3038 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692178477911904256) | true | `/test pull-kwok-verify-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-verify-main | 010a53ae615b93ba017e6e9e952da1a9d93f3038 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-verify-main/1692178477911904256) | true | `/test pull-kwok-verify-main`
pull-kwok-e2e-test-main | 010a53ae615b93ba017e6e9e952da1a9d93f3038 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-e2e-test-main/1692178478130008064) | true | `/test pull-kwok-e2e-test-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-unit-test-main | 06adb18393508ff00257d85fdce2435b040c15d0 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-unit-test-main/1692191808357928960) | true | `/test pull-kwok-unit-test-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @wzshiming: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-kwok-unit-test-main | 06adb18393508ff00257d85fdce2435b040c15d0 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-unit-test-main/1692191808357928960) | true | `/test pull-kwok-unit-test-main`
pull-kwok-e2e-test-main | 06adb18393508ff00257d85fdce2435b040c15d0 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_kwok/774/pull-kwok-e2e-test-main/1692191808429232128) | true | `/test pull-kwok-e2e-test-main`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=kwok&pr=774). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Awzshiming). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/kwok/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/.golangci.yaml b/.golangci.yaml
index 12a876f3..9af9432e 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -27,7 +27,6 @@ linters:
     - nolintlint
     - prealloc
     - revive
-    # - rowserrcheck
     - staticcheck
     - stylecheck
     - typecheck
@@ -75,6 +74,9 @@ linters-settings:
       - k8s.io/klog: "please use `sigs.k8s.io/kwok/pkg/log instead"
       - golang.org/x/exp/slog: "please use `sigs.k8s.io/kwok/pkg/log` instead"
       - log: "please use `sigs.k8s.io/kwok/pkg/log` instead"
+      # Use sigs.k8s.io/kwok/pkg/utils/yaml, which is a wrapper around yaml
+      - k8s.io/apimachinery/pkg/util/yaml: "please use `sigs.k8s.io/kwok/pkg/utils/yaml` instead"
+      - sigs.k8s.io/yaml: "please use `sigs.k8s.io/kwok/pkg/utils/yaml` instead"
       # Use sigs.k8s.io/kwok/pkg/utils/wait, which is a wrapper around wait
       - k8s.io/apimachinery/pkg/util/wait: "please use `sigs.k8s.io/kwok/pkg/utils/wait` instead"
       # Use sigs.k8s.io/kwok/pkg/utils/slices, which is a wrapper around slices
@@ -106,8 +108,10 @@ issues:
         - revive
       source: _ "embed"
 run:
+  concurrency: 1
   timeout: 10m
-  allow-parallel-runners: true
+  allow-parallel-runners: false
   skip-dirs-use-default: true
   skip-files:
     - ".*\\.zz_generated\\..*"
+  go: '1.20'
diff --git a/go.mod b/go.mod
index 6df1a772..195eca7e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,7 @@ module sigs.k8s.io/kwok
 go 1.20
 
 require (
-	github.com/ahmetb/gen-crd-api-reference-docs v0.3.0
 	github.com/blang/semver/v4 v4.0.0
-	github.com/client9/misspell v0.3.4
 	github.com/compose-spec/compose-go v1.8.2 // fixation
 	github.com/containerd/go-cni v1.1.9
 	github.com/containernetworking/plugins v1.3.0
@@ -28,7 +26,6 @@ require (
 	golang.org/x/sync v0.3.0
 	golang.org/x/sys v0.11.0
 	golang.org/x/term v0.11.0
-	gopkg.in/yaml.v3 v3.0.1
 	k8s.io/api v0.28.0
 	k8s.io/apimachinery v0.28.0
 	k8s.io/apiserver v0.28.0
@@ -37,7 +34,6 @@ require (
 	k8s.io/cri-api v0.28.0
 	k8s.io/kubelet v0.28.0
 	k8s.io/utils v0.0.0-20230726121419-3b25d923346b
-	mvdan.cc/sh/v3 v3.7.0
 	sigs.k8s.io/controller-runtime v0.15.1
 	sigs.k8s.io/controller-tools v0.12.1
 	sigs.k8s.io/kustomize/kustomize/v5 v5.1.1
@@ -66,7 +62,6 @@ require (
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/renameio/v2 v2.0.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/imdario/mergo v0.3.16 // indirect
@@ -85,10 +80,10 @@ require (
 	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/common v0.44.0 // indirect
 	github.com/prometheus/procfs v0.10.1 // indirect
+	github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/stoewer/go-strcase v1.3.0 // indirect
 	github.com/wzshiming/trie v0.1.1 // indirect
@@ -109,12 +104,11 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/apiextensions-apiserver v0.27.3 // indirect
 	k8s.io/gengo v0.0.0-20220902162205-c0856e24416d // indirect
-	k8s.io/klog v0.2.0 // indirect
 	k8s.io/klog/v2 v2.100.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
-	mvdan.cc/editorconfig v0.2.0 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/kustomize/api v0.14.0 // indirect
 	sigs.k8s.io/kustomize/cmd/config v0.11.3 // indirect
diff --git a/go.sum b/go.sum
index 4afb7aef..07869147 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,3 @@
-github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 h1:+XfOU14S4bGuwyvCijJwhhBIjYN+YXS18jrCY2EzJaY=
-github.com/ahmetb/gen-crd-api-reference-docs v0.3.0/go.mod h1:TdjdkYhlOifCQWPs1UdTma97kQQMozf5h26hTuG70u8=
 github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 h1:goHVqTbFX3AIo0tzGr14pgfAW2ZfPChKO21Z9MGf/gk=
 github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -12,8 +10,6 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/compose-spec/compose-go v1.8.2 h1:sUQvDxnPgpcOyoxC/lz7mFTrTlHeZ6LWyuASYetkOqw=
 github.com/compose-spec/compose-go v1.8.2/go.mod h1:Tb5Ae2PsYN3GTqYqzl2IRbTPiJtPZZjMw8UKUvmehFk=
 github.com/containerd/go-cni v1.1.9 h1:ORi7P1dYzCwVM6XPN4n3CbkuOx/NZ2DOqy+SHRdo9rU=
@@ -40,7 +36,6 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv
 github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
 github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
-github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -93,8 +88,6 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
-github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
-github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -160,9 +153,6 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
 github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -176,11 +166,10 @@ github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO
 github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
 github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
 github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
 github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -337,11 +326,8 @@ k8s.io/code-generator v0.28.0 h1:msdkRVJNVFgdiIJ8REl/d3cZsMB9HByFcWMmn13NyuE=
 k8s.io/code-generator v0.28.0/go.mod h1:ueeSJZJ61NHBa0ccWLey6mwawum25vX61nRZ6WOzN9A=
 k8s.io/cri-api v0.28.0 h1:TVidtHNi425IaKF50oDD5hRvQuK7wB4NQAfTVOcr9QA=
 k8s.io/cri-api v0.28.0/go.mod h1:xXygwvSOGcT/2KXg8sMYTHns2xFem3949kCQn5IS1k4=
-k8s.io/gengo v0.0.0-20201203183100-97869a43a9d9/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/gengo v0.0.0-20220902162205-c0856e24416d h1:U9tB195lKdzwqicbJvyJeOXV7Klv+wNAWENRnXEGi08=
 k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
-k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
-k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
 k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
 k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
@@ -351,10 +337,6 @@ k8s.io/kubelet v0.28.0 h1:H/3JAkLIungVF+WLpqrxhgJ4gzwsbN8VA8LOTYsEX3U=
 k8s.io/kubelet v0.28.0/go.mod h1:i8jUg4ltbRusT3ExOhSAeqETuHdoHTZcTT2cPr9RTgc=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-mvdan.cc/editorconfig v0.2.0 h1:XL+7ys6ls/RKrkUNFQvEwIvNHh+JKx8Mj1pUV5wQxQE=
-mvdan.cc/editorconfig v0.2.0/go.mod h1:lvnnD3BNdBYkhq+B4uBuFFKatfp02eB6HixDvEz91C0=
-mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
-mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8=
 sigs.k8s.io/controller-runtime v0.15.1 h1:9UvgKD4ZJGcj24vefUFgZFP3xej/3igL9BsOUTb/+4c=
 sigs.k8s.io/controller-runtime v0.15.1/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk=
 sigs.k8s.io/controller-tools v0.12.1 h1:GyQqxzH5wksa4n3YDIJdJJOopztR5VDM+7qsyg5yE4U=
diff --git a/hack/tools/tools.go b/hack/tools/tools.go
index 497535fb..ed3c1733 100644
--- a/hack/tools/tools.go
+++ b/hack/tools/tools.go
@@ -28,15 +28,6 @@ import (
 	// controller-gen
 	_ "sigs.k8s.io/controller-tools/cmd/controller-gen"
 
-	// gen-crd-api-reference-docs
-	_ "github.com/ahmetb/gen-crd-api-reference-docs"
-
-	// shfmt
-	_ "mvdan.cc/sh/v3/cmd/shfmt"
-
-	// misspell
-	_ "github.com/client9/misspell/cmd/misspell"
-
 	// kustomize
 	_ "sigs.k8s.io/kustomize/kustomize/v5"
 )
diff --git a/hack/update-all.sh b/hack/update-all.sh
index b29c5aa4..1095ece5 100755
--- a/hack/update-all.sh
+++ b/hack/update-all.sh
@@ -43,6 +43,11 @@ if [[ "${UPDATE_GO_MOD:-true}" == "true" ]]; then
   "${ROOT_DIR}"/hack/update-go-mod.sh || failed+=(go-mod)
 fi
 
+if [[ "${UPDATE_GO_LINT:-true}" == "true" ]]; then
+  echo "[*] Update go lint..."
+  "${ROOT_DIR}"/hack/update-go-lint.sh || failed+=(go-lint)
+fi
+
 if [[ "${UPDATE_ENDS_NEWLINE:-true}" == "true" ]]; then
   echo "[*] Update ends newline..."
   "${ROOT_DIR}"/hack/update-ends-newline.sh || failed+=(ends-newline)
diff --git a/hack/update-api-docs.sh b/hack/update-api-docs.sh
index 7ca3e0e0..1f897fb7 100755
--- a/hack/update-api-docs.sh
+++ b/hack/update-api-docs.sh
@@ -24,7 +24,7 @@ ROOT_DIR="$(realpath "${DIR}/..")"
 function gendoc() {
   local confdir="${ROOT_DIR}/hack/api_docs"
 
-  go run github.com/ahmetb/gen-crd-api-reference-docs \
+  go run github.com/ahmetb/gen-crd-api-reference-docs@v0.3.0 \
     -template-dir "${confdir}" \
     -config "${confdir}/config.json" \
     "$@"
diff --git a/hack/update-go-lint.sh b/hack/update-go-lint.sh
new file mode 100755
index 00000000..874857e9
--- /dev/null
+++ b/hack/update-go-lint.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Copyright 2023 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+DIR="$(dirname "${BASH_SOURCE[0]}")"
+
+ROOT_DIR="$(realpath "${DIR}/..")"
+
+COMMAND=(go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.2)
+
+cd "${ROOT_DIR}" && "${COMMAND[@]}" run -c "${ROOT_DIR}/.golangci.yaml" --fix
diff --git a/hack/update-shell-format.sh b/hack/update-shell-format.sh
index 5f9d17f2..2e8069a7 100755
--- a/hack/update-shell-format.sh
+++ b/hack/update-shell-format.sh
@@ -31,7 +31,7 @@ function format() {
     -o -path ./demo/node_modules/\* \
     -o -path ./site/themes/\* \
     \))
-  go run mvdan.cc/sh/v3/cmd/shfmt -w -i=2 "${findfiles[@]}"
+  go run mvdan.cc/sh/v3/cmd/shfmt@v3.7.0 -w -i=2 "${findfiles[@]}"
 }
 
 cd "${ROOT_DIR}" && format
diff --git a/hack/update-spelling.sh b/hack/update-spelling.sh
index 30441fe6..b04dc3c5 100755
--- a/hack/update-spelling.sh
+++ b/hack/update-spelling.sh
@@ -40,7 +40,7 @@ function update() {
     -o -path ./demo/node_modules/\* \
     -o -path ./site/themes/\* \
     \))
-  go run github.com/client9/misspell/cmd/misspell \
+  go run github.com/client9/misspell/cmd/misspell@v0.3.4 \
     -locale US -w -i "${ignore}" "${files[@]}"
 }
 
diff --git a/hack/verify-all.sh b/hack/verify-all.sh
index 5a00988f..8918ddaf 100755
--- a/hack/verify-all.sh
+++ b/hack/verify-all.sh
@@ -43,6 +43,11 @@ if [[ "${VERIFY_GO_FORMAT:-true}" == "true" ]]; then
   "${ROOT_DIR}"/hack/verify-go-format.sh || failed+=(go-format)
 fi
 
+if [[ "${VERIFY_GO_LINT:-true}" == "true" ]]; then
+  echo "[*] Verifying go lint..."
+  "${ROOT_DIR}"/hack/verify-go-lint.sh || failed+=(go-lint)
+fi
+
 if [[ "${VERIFY_CODEGEN:-true}" == "true" ]]; then
   echo "[*] Verifying codegen..."
   "${ROOT_DIR}"/hack/verify-codegen.sh || failed+=(codegen)
diff --git a/hack/verify-go-lint.sh b/hack/verify-go-lint.sh
new file mode 100755
index 00000000..6c7ddeb3
--- /dev/null
+++ b/hack/verify-go-lint.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Copyright 2023 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+DIR="$(dirname "${BASH_SOURCE[0]}")"
+
+ROOT_DIR="$(realpath "${DIR}/..")"
+
+COMMAND=(go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.2)
+
+cd "${ROOT_DIR}" && "${COMMAND[@]}" run -c "${ROOT_DIR}/.golangci.yaml"
diff --git a/kustomize/crd/bases/kwok.x-k8s.io_attaches.yaml b/kustomize/crd/bases/kwok.x-k8s.io_attaches.yaml
index 6827282f..2afad92d 100644
--- a/kustomize/crd/bases/kwok.x-k8s.io_attaches.yaml
+++ b/kustomize/crd/bases/kwok.x-k8s.io_attaches.yaml
@@ -46,11 +46,7 @@ spec:
                       type: array
                     logsFile:
                       description: LogsFile is the file from which the attach starts
-                      minLength: 1
                       type: string
-                  required:
-                  - containers
-                  - logsFile
                   type: object
                 type: array
             required:
diff --git a/kustomize/crd/bases/kwok.x-k8s.io_clusterattaches.yaml b/kustomize/crd/bases/kwok.x-k8s.io_clusterattaches.yaml
index 14ec3cb0..5264222e 100644
--- a/kustomize/crd/bases/kwok.x-k8s.io_clusterattaches.yaml
+++ b/kustomize/crd/bases/kwok.x-k8s.io_clusterattaches.yaml
@@ -46,11 +46,7 @@ spec:
                       type: array
                     logsFile:
                       description: LogsFile is the file from which the attach starts
-                      minLength: 1
                       type: string
-                  required:
-                  - containers
-                  - logsFile
                   type: object
                 type: array
               selector:
diff --git a/kustomize/crd/bases/kwok.x-k8s.io_clusterlogs.yaml b/kustomize/crd/bases/kwok.x-k8s.io_clusterlogs.yaml
index 3546c06b..9b54ea0c 100644
--- a/kustomize/crd/bases/kwok.x-k8s.io_clusterlogs.yaml
+++ b/kustomize/crd/bases/kwok.x-k8s.io_clusterlogs.yaml
@@ -50,12 +50,7 @@ spec:
                     logsFile:
                       description: LogsFile is the file from which the log forward
                         starts
-                      minLength: 1
                       type: string
-                  required:
-                  - containers
-                  - follow
-                  - logsFile
                   type: object
                 type: array
               selector:
diff --git a/kustomize/crd/bases/kwok.x-k8s.io_logs.yaml b/kustomize/crd/bases/kwok.x-k8s.io_logs.yaml
index b9436f0a..ddb269b1 100644
--- a/kustomize/crd/bases/kwok.x-k8s.io_logs.yaml
+++ b/kustomize/crd/bases/kwok.x-k8s.io_logs.yaml
@@ -50,12 +50,7 @@ spec:
                     logsFile:
                       description: LogsFile is the file from which the log forward
                         starts
-                      minLength: 1
                       type: string
-                  required:
-                  - containers
-                  - follow
-                  - logsFile
                   type: object
                 type: array
             required:
diff --git a/pkg/apis/internalversion/zz_generated.conversion.go b/pkg/apis/internalversion/zz_generated.conversion.go
index 50bede41..fdf7af48 100644
--- a/pkg/apis/internalversion/zz_generated.conversion.go
+++ b/pkg/apis/internalversion/zz_generated.conversion.go
@@ -602,7 +602,9 @@ func Convert_v1alpha1_Attach_To_internalversion_Attach(in *v1alpha1.Attach, out
 
 func autoConvert_internalversion_AttachConfig_To_v1alpha1_AttachConfig(in *AttachConfig, out *v1alpha1.AttachConfig, s conversion.Scope) error {
 	out.Containers = *(*[]string)(unsafe.Pointer(&in.Containers))
-	out.LogsFile = in.LogsFile
+	if err := v1.Convert_string_To_Pointer_string(&in.LogsFile, &out.LogsFile, s); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -613,7 +615,9 @@ func Convert_internalversion_AttachConfig_To_v1alpha1_AttachConfig(in *AttachCon
 
 func autoConvert_v1alpha1_AttachConfig_To_internalversion_AttachConfig(in *v1alpha1.AttachConfig, out *AttachConfig, s conversion.Scope) error {
 	out.Containers = *(*[]string)(unsafe.Pointer(&in.Containers))
-	out.LogsFile = in.LogsFile
+	if err := v1.Convert_Pointer_string_To_string(&in.LogsFile, &out.LogsFile, s); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -623,7 +627,17 @@ func Convert_v1alpha1_AttachConfig_To_internalversion_AttachConfig(in *v1alpha1.
 }
 
 func autoConvert_internalversion_AttachSpec_To_v1alpha1_AttachSpec(in *AttachSpec, out *v1alpha1.AttachSpec, s conversion.Scope) error {
-	out.Attaches = *(*[]v1alpha1.AttachConfig)(unsafe.Pointer(&in.Attaches))
+	if in.Attaches != nil {
+		in, out := &in.Attaches, &out.Attaches
+		*out = make([]v1alpha1.AttachConfig, len(*in))
+		for i := range *in {
+			if err := Convert_internalversion_AttachConfig_To_v1alpha1_AttachConfig(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.Attaches = nil
+	}
 	return nil
 }
 
@@ -633,7 +647,17 @@ func Convert_internalversion_AttachSpec_To_v1alpha1_AttachSpec(in *AttachSpec, o
 }
 
 func autoConvert_v1alpha1_AttachSpec_To_internalversion_AttachSpec(in *v1alpha1.AttachSpec, out *AttachSpec, s conversion.Scope) error {
-	out.Attaches = *(*[]AttachConfig)(unsafe.Pointer(&in.Attaches))
+	if in.Attaches != nil {
+		in, out := &in.Attaches, &out.Attaches
+		*out = make([]AttachConfig, len(*in))
+		for i := range *in {
+			if err := Convert_v1alpha1_AttachConfig_To_internalversion_AttachConfig(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.Attaches = nil
+	}
 	return nil
 }
 
@@ -672,7 +696,17 @@ func Convert_v1alpha1_ClusterAttach_To_internalversion_ClusterAttach(in *v1alpha
 
 func autoConvert_internalversion_ClusterAttachSpec_To_v1alpha1_ClusterAttachSpec(in *ClusterAttachSpec, out *v1alpha1.ClusterAttachSpec, s conversion.Scope) error {
 	out.Selector = (*v1alpha1.ObjectSelector)(unsafe.Pointer(in.Selector))
-	out.Attaches = *(*[]v1alpha1.AttachConfig)(unsafe.Pointer(&in.Attaches))
+	if in.Attaches != nil {
+		in, out := &in.Attaches, &out.Attaches
+		*out = make([]v1alpha1.AttachConfig, len(*in))
+		for i := range *in {
+			if err := Convert_internalversion_AttachConfig_To_v1alpha1_AttachConfig(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.Attaches = nil
+	}
 	return nil
 }
 
@@ -683,7 +717,17 @@ func Convert_internalversion_ClusterAttachSpec_To_v1alpha1_ClusterAttachSpec(in
 
 func autoConvert_v1alpha1_ClusterAttachSpec_To_internalversion_ClusterAttachSpec(in *v1alpha1.ClusterAttachSpec, out *ClusterAttachSpec, s conversion.Scope) error {
 	out.Selector = (*ObjectSelector)(unsafe.Pointer(in.Selector))
-	out.Attaches = *(*[]AttachConfig)(unsafe.Pointer(&in.Attaches))
+	if in.Attaches != nil {
+		in, out := &in.Attaches, &out.Attaches
+		*out = make([]AttachConfig, len(*in))
+		for i := range *in {
+			if err := Convert_v1alpha1_AttachConfig_To_internalversion_AttachConfig(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.Attaches = nil
+	}
 	return nil
 }
 
@@ -772,7 +816,17 @@ func Convert_v1alpha1_ClusterLogs_To_internalversion_ClusterLogs(in *v1alpha1.Cl
 
 func autoConvert_internalversion_ClusterLogsSpec_To_v1alpha1_ClusterLogsSpec(in *ClusterLogsSpec, out *v1alpha1.ClusterLogsSpec, s conversion.Scope) error {
 	out.Selector = (*v1alpha1.ObjectSelector)(unsafe.Pointer(in.Selector))
-	out.Logs = *(*[]v1alpha1.Log)(unsafe.Pointer(&in.Logs))
+	if in.Logs != nil {
+		in, out := &in.Logs, &out.Logs
+		*out = make([]v1alpha1.Log, len(*in))
+		for i := range *in {
+			if err := Convert_internalversion_Log_To_v1alpha1_Log(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.Logs = nil
+	}
 	return nil
 }
 
@@ -783,7 +837,17 @@ func Convert_internalversion_ClusterLogsSpec_To_v1alpha1_ClusterLogsSpec(in *Clu
 
 func autoConvert_v1alpha1_ClusterLogsSpec_To_internalversion_ClusterLogsSpec(in *v1alpha1.ClusterLogsSpec, out *ClusterLogsSpec, s conversion.Scope) error {
 	out.Selector = (*ObjectSelector)(unsafe.Pointer(in.Selector))
-	out.Logs = *(*[]Log)(unsafe.Pointer(&in.Logs))
+	if in.Logs != nil {
+		in, out := &in.Logs, &out.Logs
+		*out = make([]Log, len(*in))
+		for i := range *in {
+			if err := Convert_v1alpha1_Log_To_internalversion_Log(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.Logs = nil
+	}
 	return nil
 }
 
@@ -1613,8 +1677,12 @@ func Convert_v1alpha1_KwokctlResource_To_internalversion_KwokctlResource(in *con
 
 func autoConvert_internalversion_Log_To_v1alpha1_Log(in *Log, out *v1alpha1.Log, s conversion.Scope) error {
 	out.Containers = *(*[]string)(unsafe.Pointer(&in.Containers))
-	out.LogsFile = in.LogsFile
-	out.Follow = in.Follow
+	if err := v1.Convert_string_To_Pointer_string(&in.LogsFile, &out.LogsFile, s); err != nil {
+		return err
+	}
+	if err := v1.Convert_bool_To_Pointer_bool(&in.Follow, &out.Follow, s); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -1625,8 +1693,12 @@ func Convert_internalversion_Log_To_v1alpha1_Log(in *Log, out *v1alpha1.Log, s c
 
 func autoConvert_v1alpha1_Log_To_internalversion_Log(in *v1alpha1.Log, out *Log, s conversion.Scope) error {
 	out.Containers = *(*[]string)(unsafe.Pointer(&in.Containers))
-	out.LogsFile = in.LogsFile
-	out.Follow = in.Follow
+	if err := v1.Convert_Pointer_string_To_string(&in.LogsFile, &out.LogsFile, s); err != nil {
+		return err
+	}
+	if err := v1.Convert_Pointer_bool_To_bool(&in.Follow, &out.Follow, s); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -1664,7 +1736,17 @@ func Convert_v1alpha1_Logs_To_internalversion_Logs(in *v1alpha1.Logs, out *Logs,
 }
 
 func autoConvert_internalversion_LogsSpec_To_v1alpha1_LogsSpec(in *LogsSpec, out *v1alpha1.LogsSpec, s conversion.Scope) error {
-	out.Logs = *(*[]v1alpha1.Log)(unsafe.Pointer(&in.Logs))
+	if in.Logs != nil {
+		in, out := &in.Logs, &out.Logs
+		*out = make([]v1alpha1.Log, len(*in))
+		for i := range *in {
+			if err := Convert_internalversion_Log_To_v1alpha1_Log(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.Logs = nil
+	}
 	return nil
 }
 
@@ -1674,7 +1756,17 @@ func Convert_internalversion_LogsSpec_To_v1alpha1_LogsSpec(in *LogsSpec, out *v1
 }
 
 func autoConvert_v1alpha1_LogsSpec_To_internalversion_LogsSpec(in *v1alpha1.LogsSpec, out *LogsSpec, s conversion.Scope) error {
-	out.Logs = *(*[]Log)(unsafe.Pointer(&in.Logs))
+	if in.Logs != nil {
+		in, out := &in.Logs, &out.Logs
+		*out = make([]Log, len(*in))
+		for i := range *in {
+			if err := Convert_v1alpha1_Log_To_internalversion_Log(&(*in)[i], &(*out)[i], s); err != nil {
+				return err
+			}
+		}
+	} else {
+		out.Logs = nil
+	}
 	return nil
 }
 
diff --git a/pkg/apis/v1alpha1/attach_types.go b/pkg/apis/v1alpha1/attach_types.go
index 95d7a8cf..8da79c2b 100644
--- a/pkg/apis/v1alpha1/attach_types.go
+++ b/pkg/apis/v1alpha1/attach_types.go
@@ -63,11 +63,9 @@ type AttachSpec struct {
 // AttachConfig holds information how to attach.
 type AttachConfig struct {
 	// Containers is list of container names.
-	Containers []string `json:"containers"`
+	Containers []string `json:"containers,omitempty"`
 	// LogsFile is the file from which the attach starts
-	// +kubebuilder:validation:Required
-	// +kubebuilder:validation:MinLength=1
-	LogsFile string `json:"logsFile"`
+	LogsFile *string `json:"logsFile,omitempty"`
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
diff --git a/pkg/apis/v1alpha1/logs_types.go b/pkg/apis/v1alpha1/logs_types.go
index f41811cb..0d59717f 100644
--- a/pkg/apis/v1alpha1/logs_types.go
+++ b/pkg/apis/v1alpha1/logs_types.go
@@ -63,13 +63,11 @@ type LogsSpec struct {
 // Log holds information how to forward logs.
 type Log struct {
 	// Containers is list of container names.
-	Containers []string `json:"containers"`
+	Containers []string `json:"containers,omitempty"`
 	// LogsFile is the file from which the log forward starts
-	// +kubebuilder:validation:Required
-	// +kubebuilder:validation:MinLength=1
-	LogsFile string `json:"logsFile"`
+	LogsFile *string `json:"logsFile,omitempty"`
 	// Follow up if true
-	Follow bool `json:"follow"`
+	Follow *bool `json:"follow,omitempty"`
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
diff --git a/pkg/apis/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/v1alpha1/zz_generated.deepcopy.go
index 9fd80dc9..ded7eca0 100644
--- a/pkg/apis/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/v1alpha1/zz_generated.deepcopy.go
@@ -61,6 +61,11 @@ func (in *AttachConfig) DeepCopyInto(out *AttachConfig) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
+	if in.LogsFile != nil {
+		in, out := &in.LogsFile, &out.LogsFile
+		*out = new(string)
+		**out = **in
+	}
 	return
 }
 
@@ -880,6 +885,16 @@ func (in *Log) DeepCopyInto(out *Log) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
+	if in.LogsFile != nil {
+		in, out := &in.LogsFile, &out.LogsFile
+		*out = new(string)
+		**out = **in
+	}
+	if in.Follow != nil {
+		in, out := &in.Follow, &out.Follow
+		*out = new(bool)
+		**out = **in
+	}
 	return
 }
 
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 7f50023b..835a177b 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -28,8 +28,6 @@ import (
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	utilyaml "k8s.io/apimachinery/pkg/util/yaml"
-	"sigs.k8s.io/yaml"
 
 	configv1alpha1 "sigs.k8s.io/kwok/pkg/apis/config/v1alpha1"
 	"sigs.k8s.io/kwok/pkg/apis/internalversion"
@@ -40,6 +38,7 @@ import (
 	"sigs.k8s.io/kwok/pkg/utils/maps"
 	"sigs.k8s.io/kwok/pkg/utils/patch"
 	"sigs.k8s.io/kwok/pkg/utils/path"
+	"sigs.k8s.io/kwok/pkg/utils/yaml"
 )
 
 var (
@@ -389,10 +388,25 @@ func SaveTo(ctx context.Context, w io.Writer, objs []InternalObject) error {
 	return nil
 }
 
+// UnmarshalWithType unmarshals the given raw message into the internal object.
+func UnmarshalWithType[T InternalObject, D string | []byte](raw D) (t T, err error) {
+	obj, err := Unmarshal(raw)
+	if err != nil {
+		return t, err
+	}
+	t, ok := obj.(T)
+	if !ok {
+		return t, fmt.Errorf("unexpected type %T %s", obj, log.KObj(obj))
+	}
+	return t, nil
+}
+
 // Unmarshal unmarshals the given raw message into the internal object.
-func Unmarshal(raw []byte) (InternalObject, error) {
+func Unmarshal[D string | []byte](d D) (InternalObject, error) {
 	meta := metav1.TypeMeta{}
 
+	raw := []byte(d)
+
 	raw, err := yaml.YAMLToJSON(raw)
 	if err != nil {
 		return nil, err
@@ -516,7 +530,7 @@ func loadRawFromFile(p string) ([]json.RawMessage, error) {
 
 func loadRaw(r io.Reader) ([]json.RawMessage, error) {
 	var raws []json.RawMessage
-	decoder := utilyaml.NewYAMLToJSONDecoder(r)
+	decoder := yaml.NewDecoder(r)
 	for {
 		var raw json.RawMessage
 		err := decoder.Decode(&raw)
diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go
index 4964924c..88586fef 100644
--- a/pkg/consts/consts.go
+++ b/pkg/consts/consts.go
@@ -45,8 +45,9 @@ var (
 	DockerComposeVersion      = "2.17.2"
 	DockerComposeBinaryPrefix = "https://github.com/docker/compose/releases/download"
 
-	KindVersion      = "0.19.0"
-	KindBinaryPrefix = "https://github.com/kubernetes-sigs/kind/releases/download"
+	KindVersion         = "0.19.0"
+	KindBinaryPrefix    = "https://github.com/kubernetes-sigs/kind/releases/download"
+	KindNodeImagePrefix = "docker.io/kindest"
 
 	DashboardVersion      = "2.7.0"
 	DashboardBinaryPrefix = ""
@@ -60,8 +61,6 @@ var (
 	JaegerBinaryPrefix = "https://github.com/jaegertracing/jaeger/releases/download"
 	JaegerImagePrefix  = "docker.io/jaegertracing"
 
-	KindNodeImagePrefix = "docker.io/kindest"
-
 	DefaultUnlimitedQPS   = 5000.0
 	DefaultUnlimitedBurst = 10000
 )
diff --git a/pkg/kwok/cmd/root.go b/pkg/kwok/cmd/root.go
index 1539daf9..9877650f 100644
--- a/pkg/kwok/cmd/root.go
+++ b/pkg/kwok/cmd/root.go
@@ -286,13 +286,18 @@ func runE(ctx context.Context, flags *flagpole) error {
 		return err
 	}
 
+	err = ctr.Start(ctx)
+	if err != nil {
+		return err
+	}
+
 	serverAddress := flags.Options.ServerAddress
 	if serverAddress == "" && flags.Options.NodePort != 0 {
 		serverAddress = "0.0.0.0:" + format.String(flags.Options.NodePort)
 	}
 
 	if serverAddress != "" {
-		config := server.Config{
+		conf := server.Config{
 			TypedKwokClient:     typedKwokClient,
 			EnableCRDs:          flags.Options.EnableCRDs,
 			ClusterPortForwards: clusterPortForwards,
@@ -304,9 +309,11 @@ func runE(ctx context.Context, flags *flagpole) error {
 			ClusterAttaches:     clusterAttaches,
 			Attaches:            attaches,
 			Metrics:             metrics,
-			Controller:          ctr,
+			DataSource:          ctr,
+			NodeCacheGetter:     ctr.GetNodeCache(),
+			PodCacheGetter:      ctr.GetPodCache(),
 		}
-		svc, err := server.NewServer(config)
+		svc, err := server.NewServer(conf)
 		if err != nil {
 			return fmt.Errorf("failed to create server: %w", err)
 		}
@@ -347,11 +354,6 @@ func runE(ctx context.Context, flags *flagpole) error {
 		}()
 	}
 
-	err = ctr.Start(ctx)
-	if err != nil {
-		return err
-	}
-
 	<-ctx.Done()
 	return nil
 }
@@ -401,14 +403,10 @@ func waitForReady(ctx context.Context, clientset kubernetes.Interface) error {
 
 func getDefaultNodeStages(lease bool) ([]*internalversion.Stage, error) {
 	nodeStages := []*internalversion.Stage{}
-	nodeInit, err := config.Unmarshal([]byte(nodefast.DefaultNodeInit))
+	nodeInitStage, err := config.UnmarshalWithType[*internalversion.Stage](nodefast.DefaultNodeInit)
 	if err != nil {
 		return nil, err
 	}
-	nodeInitStage, ok := nodeInit.(*internalversion.Stage)
-	if !ok {
-		return nil, fmt.Errorf("failed to convert node init to stage")
-	}
 	nodeStages = append(nodeStages, nodeInitStage)
 
 	rawHeartbeat := nodeheartbeat.DefaultNodeHeartbeat
@@ -416,14 +414,10 @@ func getDefaultNodeStages(lease bool) ([]*internalversion.Stage, error) {
 		rawHeartbeat = nodeheartbeatwithlease.DefaultNodeHeartbeatWithLease
 	}
 
-	nodeHeartbeat, err := config.Unmarshal([]byte(rawHeartbeat))
+	nodeHeartbeatStage, err := config.UnmarshalWithType[*internalversion.Stage](rawHeartbeat)
 	if err != nil {
 		return nil, err
 	}
-	nodeHeartbeatStage, ok := nodeHeartbeat.(*internalversion.Stage)
-	if !ok {
-		return nil, fmt.Errorf("failed to convert node init to stage")
-	}
 	nodeStages = append(nodeStages, nodeHeartbeatStage)
 	return nodeStages, nil
 }
@@ -433,16 +427,5 @@ func getDefaultPodStages() ([]*internalversion.Stage, error) {
 		podfast.DefaultPodReady,
 		podfast.DefaultPodComplete,
 		podfast.DefaultPodDelete,
-	}, func(s string) (*internalversion.Stage, error) {
-		iobj, err := config.Unmarshal([]byte(s))
-		if err != nil {
-			return nil, err
-		}
-		stage, ok := iobj.(*internalversion.Stage)
-		if !ok {
-			return nil, fmt.Errorf("failed to convert pod init to stage")
-		}
-
-		return stage, nil
-	})
+	}, config.UnmarshalWithType[*internalversion.Stage, string])
 }
diff --git a/pkg/kwok/controllers/controller.go b/pkg/kwok/controllers/controller.go
index 966273bf..2d544738 100644
--- a/pkg/kwok/controllers/controller.go
+++ b/pkg/kwok/controllers/controller.go
@@ -35,7 +35,6 @@ import (
 	clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/utils/clock"
-	"sigs.k8s.io/yaml"
 
 	"sigs.k8s.io/kwok/pkg/apis/internalversion"
 	"sigs.k8s.io/kwok/pkg/apis/v1alpha1"
@@ -47,6 +46,7 @@ import (
 	"sigs.k8s.io/kwok/pkg/utils/informer"
 	"sigs.k8s.io/kwok/pkg/utils/queue"
 	"sigs.k8s.io/kwok/pkg/utils/slices"
+	"sigs.k8s.io/kwok/pkg/utils/yaml"
 )
 
 var (
@@ -483,11 +483,6 @@ func (c *Controller) Start(ctx context.Context) error {
 	return nil
 }
 
-// GetNodeInfo returns the node info for the given node
-func (c *Controller) GetNodeInfo(nodeName string) (*NodeInfo, bool) {
-	return c.nodes.Get(nodeName)
-}
-
 // ListNodes returns all nodes
 func (c *Controller) ListNodes() []string {
 	return c.nodes.List()
@@ -508,6 +503,15 @@ func (c *Controller) GetNodeCache() informer.Getter[*corev1.Node] {
 	return c.nodeCacheGetter
 }
 
+// StartedContainersTotal returns the total number of containers started
+func (c *Controller) StartedContainersTotal(nodeName string) int64 {
+	nodeInfo, ok := c.nodes.Get(nodeName)
+	if !ok {
+		return 0
+	}
+	return nodeInfo.StartedContainer.Load()
+}
+
 // Identity returns a unique identifier for this controller
 func Identity() (string, error) {
 	hostname, err := os.Hostname()
diff --git a/pkg/kwok/controllers/node_controller.go b/pkg/kwok/controllers/node_controller.go
index 1bffde66..7ac9b2f5 100644
--- a/pkg/kwok/controllers/node_controller.go
+++ b/pkg/kwok/controllers/node_controller.go
@@ -167,17 +167,15 @@ func NewNodeController(conf NodeControllerConfig) (*NodeController, error) {
 		readOnlyFunc:                          conf.ReadOnlyFunc,
 		enableMetrics:                         conf.EnableMetrics,
 	}
-	funcMap := gotpl.FuncMap{
+
+	funcMap := maps.Merge(gotpl.FuncMap{
 		"NodeIP":   c.funcNodeIP,
 		"NodeName": c.funcNodeName,
 		"NodePort": c.funcNodePort,
 		"NodeConditions": func() interface{} {
 			return nodeConditionsData
 		},
-	}
-	for k, v := range conf.FuncMap {
-		funcMap[k] = v
-	}
+	}, conf.FuncMap)
 	c.renderer = gotpl.NewRenderer(funcMap)
 	return c, nil
 }
diff --git a/pkg/kwok/controllers/pod_controller.go b/pkg/kwok/controllers/pod_controller.go
index 4230e315..7855e770 100644
--- a/pkg/kwok/controllers/pod_controller.go
+++ b/pkg/kwok/controllers/pod_controller.go
@@ -134,15 +134,12 @@ func NewPodController(conf PodControllerConfig) (*PodController, error) {
 		readOnlyFunc:                          conf.ReadOnlyFunc,
 		enableMetrics:                         conf.EnableMetrics,
 	}
-	funcMap := gotpl.FuncMap{
+	funcMap := maps.Merge(gotpl.FuncMap{
 		"NodeIP":     c.funcNodeIP,
 		"PodIP":      c.funcPodIP,
 		"NodeIPWith": c.funcNodeIPWith,
 		"PodIPWith":  c.funcPodIPWith,
-	}
-	for k, v := range conf.FuncMap {
-		funcMap[k] = v
-	}
+	}, conf.FuncMap)
 	c.renderer = gotpl.NewRenderer(funcMap)
 	return c, nil
 }
diff --git a/pkg/kwok/metrics/metrics.go b/pkg/kwok/metrics/metrics.go
index d5f56529..88cb1390 100644
--- a/pkg/kwok/metrics/metrics.go
+++ b/pkg/kwok/metrics/metrics.go
@@ -28,7 +28,6 @@ import (
 	corev1 "k8s.io/api/core/v1"
 
 	"sigs.k8s.io/kwok/pkg/apis/internalversion"
-	"sigs.k8s.io/kwok/pkg/kwok/controllers"
 	"sigs.k8s.io/kwok/pkg/kwok/metrics/cel"
 	"sigs.k8s.io/kwok/pkg/log"
 	"sigs.k8s.io/kwok/pkg/utils/informer"
@@ -37,7 +36,7 @@ import (
 
 // UpdateHandler handles updating metrics on request
 type UpdateHandler struct {
-	controller      *controllers.Controller
+	dataSource      DataSource
 	environment     *cel.Environment
 	nodeCacheGetter informer.Getter[*corev1.Node]
 	podCacheGetter  informer.Getter[*corev1.Pod]
@@ -50,10 +49,17 @@ type UpdateHandler struct {
 	histograms maps.SyncMap[string, Histogram]
 }
 
+// DataSource is the interface for getting data for metrics
+type DataSource interface {
+	ListPods(nodeName string) ([]log.ObjectRef, bool)
+}
+
 // UpdateHandlerConfig is configuration for a single node
 type UpdateHandlerConfig struct {
-	Controller  *controllers.Controller
-	Environment *cel.Environment
+	DataSource      DataSource
+	Environment     *cel.Environment
+	NodeCacheGetter informer.Getter[*corev1.Node]
+	PodCacheGetter  informer.Getter[*corev1.Pod]
 }
 
 // NewMetricsUpdateHandler creates new metric update handler based on the config
@@ -64,21 +70,16 @@ func NewMetricsUpdateHandler(conf UpdateHandlerConfig) *UpdateHandler {
 	)
 
 	h := &UpdateHandler{
-		controller:      conf.Controller,
+		dataSource:      conf.DataSource,
 		environment:     conf.Environment,
-		nodeCacheGetter: conf.Controller.GetNodeCache(),
-		podCacheGetter:  conf.Controller.GetPodCache(),
+		nodeCacheGetter: conf.NodeCacheGetter,
+		podCacheGetter:  conf.PodCacheGetter,
 		registry:        registry,
 		handler:         handler,
 	}
 	return h
 }
 
-func (h *UpdateHandler) getPodsInfo(nodeName string) ([]log.ObjectRef, bool) {
-	podsInfo, ok := h.controller.ListPods(nodeName)
-	return podsInfo, ok
-}
-
 func (h *UpdateHandler) getOrRegisterGauge(metricConfig *internalversion.MetricConfig, data cel.Data) (Gauge, string, error) {
 	key, labels, err := h.createKeyAndLabels(metricConfig, data)
 	if err != nil {
@@ -196,7 +197,7 @@ func (h *UpdateHandler) updateGauge(ctx context.Context, metricConfig *internalv
 		gauge.Set(result)
 		return []string{key}, nil
 	case internalversion.DimensionPod:
-		pods, ok := h.getPodsInfo(nodeName)
+		pods, ok := h.dataSource.ListPods(nodeName)
 		if !ok {
 			logger.Warn("pods not found")
 			return nil, nil
@@ -224,7 +225,7 @@ func (h *UpdateHandler) updateGauge(ctx context.Context, metricConfig *internalv
 		}
 		return keys, nil
 	case internalversion.DimensionContainer:
-		pods, ok := h.getPodsInfo(nodeName)
+		pods, ok := h.dataSource.ListPods(nodeName)
 		if !ok {
 			logger.Warn("pods not found")
 			return nil, nil
@@ -290,7 +291,7 @@ func (h *UpdateHandler) updateCounter(ctx context.Context, metricConfig *interna
 		counter.Set(result)
 		return []string{key}, nil
 	case internalversion.DimensionPod:
-		pods, ok := h.getPodsInfo(nodeName)
+		pods, ok := h.dataSource.ListPods(nodeName)
 		if !ok {
 			logger.Warn("pods not found")
 			return nil, nil
@@ -318,7 +319,7 @@ func (h *UpdateHandler) updateCounter(ctx context.Context, metricConfig *interna
 		}
 		return keys, nil
 	case internalversion.DimensionContainer:
-		pods, ok := h.getPodsInfo(nodeName)
+		pods, ok := h.dataSource.ListPods(nodeName)
 		if !ok {
 			logger.Warn("pods not found")
 			return nil, nil
@@ -385,7 +386,7 @@ func (h *UpdateHandler) updateHistogram(ctx context.Context, metricConfig *inter
 		}
 		return []string{key}, nil
 	case internalversion.DimensionPod:
-		pods, ok := h.getPodsInfo(nodeName)
+		pods, ok := h.dataSource.ListPods(nodeName)
 		if !ok {
 			logger.Warn("pods not found")
 			return nil, nil
@@ -419,7 +420,7 @@ func (h *UpdateHandler) updateHistogram(ctx context.Context, metricConfig *inter
 		}
 		return keys, nil
 	case internalversion.DimensionContainer:
-		pods, ok := h.getPodsInfo(nodeName)
+		pods, ok := h.dataSource.ListPods(nodeName)
 		if !ok {
 			logger.Warn("pods not found")
 			return nil, nil
diff --git a/pkg/kwok/server/metrics.go b/pkg/kwok/server/metrics.go
index 9c2ade34..ad977c9a 100644
--- a/pkg/kwok/server/metrics.go
+++ b/pkg/kwok/server/metrics.go
@@ -39,17 +39,10 @@ func (s *Server) InstallMetrics(ctx context.Context) error {
 		promHandler.ServeHTTP(resp.ResponseWriter, req.Request)
 	}
 
-	controller := s.controller
 	env, err := cel.NewEnvironment(cel.NodeEvaluatorConfig{
-		EnableEvaluatorCache: true,
-		EnableResultCache:    true,
-		StartedContainersTotal: func(nodeName string) int64 {
-			nodeInfo, ok := controller.GetNodeInfo(nodeName)
-			if !ok {
-				return 0
-			}
-			return nodeInfo.StartedContainer.Load()
-		},
+		EnableEvaluatorCache:   true,
+		EnableResultCache:      true,
+		StartedContainersTotal: s.dataSource.StartedContainersTotal,
 	})
 	if err != nil {
 		return fmt.Errorf("failed to create CEL environment: %w", err)
@@ -115,8 +108,10 @@ func (s *Server) getMetrics(metric *internalversion.Metric, env *cel.Environment
 		handler, ok := s.metricsUpdateHandler.Load(nodeName)
 		if !ok {
 			handler = metrics.NewMetricsUpdateHandler(metrics.UpdateHandlerConfig{
-				Controller:  s.controller,
-				Environment: env,
+				Environment:     env,
+				DataSource:      s.dataSource,
+				NodeCacheGetter: s.nodeCacheGetter,
+				PodCacheGetter:  s.podCacheGetter,
 			})
 			s.metricsUpdateHandler.Store(nodeName, handler)
 		}
diff --git a/pkg/kwok/server/server.go b/pkg/kwok/server/server.go
index d087c51d..8b2bf101 100644
--- a/pkg/kwok/server/server.go
+++ b/pkg/kwok/server/server.go
@@ -26,15 +26,16 @@ import (
 	"github.com/emicklei/go-restful/v3"
 	"github.com/wzshiming/cmux"
 	"github.com/wzshiming/cmux/pattern"
+	corev1 "k8s.io/api/core/v1"
 	remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand"
 
 	"sigs.k8s.io/kwok/pkg/apis/internalversion"
 	"sigs.k8s.io/kwok/pkg/apis/v1alpha1"
 	"sigs.k8s.io/kwok/pkg/client/clientset/versioned"
 	"sigs.k8s.io/kwok/pkg/config/resources"
-	"sigs.k8s.io/kwok/pkg/kwok/controllers"
 	"sigs.k8s.io/kwok/pkg/kwok/metrics"
 	"sigs.k8s.io/kwok/pkg/log"
+	"sigs.k8s.io/kwok/pkg/utils/informer"
 	"sigs.k8s.io/kwok/pkg/utils/maps"
 	"sigs.k8s.io/kwok/pkg/utils/pools"
 	"sigs.k8s.io/kwok/pkg/utils/slices"
@@ -69,7 +70,16 @@ type Server struct {
 	metricsWebService    *restful.WebService
 	metricsUpdateHandler maps.SyncMap[string, *metrics.UpdateHandler]
 
-	controller *controllers.Controller
+	dataSource      DataSource
+	nodeCacheGetter informer.Getter[*corev1.Node]
+	podCacheGetter  informer.Getter[*corev1.Pod]
+}
+
+// DataSource is the interface that provides data for the server handlers.
+type DataSource interface {
+	metrics.DataSource
+	ListNodes() []string
+	StartedContainersTotal(nodeName string) int64
 }
 
 // Config holds configurations needed by the server handlers.
@@ -86,31 +96,36 @@ type Config struct {
 	ClusterAttaches     []*internalversion.ClusterAttach
 	Attaches            []*internalversion.Attach
 	Metrics             []*internalversion.Metric
-	Controller          *controllers.Controller
+
+	DataSource      DataSource
+	NodeCacheGetter informer.Getter[*corev1.Node]
+	PodCacheGetter  informer.Getter[*corev1.Pod]
 }
 
 // NewServer creates a new Server.
-func NewServer(config Config) (*Server, error) {
+func NewServer(conf Config) (*Server, error) {
 	container := restful.NewContainer()
 
 	s := &Server{
-		typedKwokClient:       config.TypedKwokClient,
-		enableCRDs:            config.EnableCRDs,
+		typedKwokClient:       conf.TypedKwokClient,
+		enableCRDs:            conf.EnableCRDs,
 		restfulCont:           container,
 		idleTimeout:           1 * time.Hour,
 		streamCreationTimeout: remotecommandconsts.DefaultStreamCreationTimeout,
 
-		clusterPortForwards: resources.NewStaticGetter(config.ClusterPortForwards),
-		portForwards:        resources.NewStaticGetter(config.PortForwards),
-		clusterExecs:        resources.NewStaticGetter(config.ClusterExecs),
-		execs:               resources.NewStaticGetter(config.Execs),
-		clusterLogs:         resources.NewStaticGetter(config.ClusterLogs),
-		logs:                resources.NewStaticGetter(config.Logs),
-		clusterAttaches:     resources.NewStaticGetter(config.ClusterAttaches),
-		attaches:            resources.NewStaticGetter(config.Attaches),
-		metrics:             resources.NewStaticGetter(config.Metrics),
-
-		controller: config.Controller,
+		clusterPortForwards: resources.NewStaticGetter(conf.ClusterPortForwards),
+		portForwards:        resources.NewStaticGetter(conf.PortForwards),
+		clusterExecs:        resources.NewStaticGetter(conf.ClusterExecs),
+		execs:               resources.NewStaticGetter(conf.Execs),
+		clusterLogs:         resources.NewStaticGetter(conf.ClusterLogs),
+		logs:                resources.NewStaticGetter(conf.Logs),
+		clusterAttaches:     resources.NewStaticGetter(conf.ClusterAttaches),
+		attaches:            resources.NewStaticGetter(conf.Attaches),
+		metrics:             resources.NewStaticGetter(conf.Metrics),
+
+		dataSource:      conf.DataSource,
+		podCacheGetter:  conf.PodCacheGetter,
+		nodeCacheGetter: conf.NodeCacheGetter,
 
 		bufPool: pools.NewPool(func() []byte {
 			return make([]byte, 32*1024)
diff --git a/pkg/kwok/server/service_discovery.go b/pkg/kwok/server/service_discovery.go
index a76d647b..c15e910d 100644
--- a/pkg/kwok/server/service_discovery.go
+++ b/pkg/kwok/server/service_discovery.go
@@ -43,7 +43,7 @@ func (s *Server) prometheusDiscovery(rw http.ResponseWriter, req *http.Request)
 	for _, m := range metrics {
 		if strings.Contains(m.Spec.Path, "{nodeName}") {
 			if listNode == nil {
-				listNode = s.controller.ListNodes()
+				listNode = s.dataSource.ListNodes()
 			}
 			for _, nodeName := range listNode {
 				targets = append(targets, prometheusStaticConfig{
diff --git a/pkg/kwokctl/cmd/logs/logs.go b/pkg/kwokctl/cmd/logs/logs.go
index 06129b36..5d7797d2 100644
--- a/pkg/kwokctl/cmd/logs/logs.go
+++ b/pkg/kwokctl/cmd/logs/logs.go
@@ -41,7 +41,7 @@ func NewCommand(ctx context.Context) *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "logs [command]",
-		Short: "Logs one of [audit, etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kwok-controller, prometheus]",
+		Short: "Logs one of [audit, etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kwok-controller, dashboard, prometheus, jaeger]",
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) == 0 {
 				return cmd.Help()
diff --git a/pkg/kwokctl/cmd/scale/scale.go b/pkg/kwokctl/cmd/scale/scale.go
index 61f85c6a..222334ce 100644
--- a/pkg/kwokctl/cmd/scale/scale.go
+++ b/pkg/kwokctl/cmd/scale/scale.go
@@ -98,25 +98,21 @@ func runE(ctx context.Context, flags *flagpole, args []string) error {
 		return krc.Name == resourceKind
 	})
 	if !ok {
-		var resourceData []byte
+		var resourceData string
 		switch resourceKind {
 		default:
 			return fmt.Errorf("resource %s is not exists", resourceKind)
 		case "pod":
-			resourceData = []byte(resource.DefaultPod)
+			resourceData = resource.DefaultPod
 		case "node":
-			resourceData = []byte(resource.DefaultNode)
+			resourceData = resource.DefaultNode
 		}
 
 		logger.Info("No resource found, use default resource", "resource", resourceKind)
-		iobj, err := config.Unmarshal(resourceData)
+		krc, err = config.UnmarshalWithType[*internalversion.KwokctlResource](resourceData)
 		if err != nil {
 			return err
 		}
-		krc, ok = iobj.(*internalversion.KwokctlResource)
-		if !ok {
-			return fmt.Errorf("resource %T is not a kwokctl resource", iobj)
-		}
 	}
 
 	parameters, err := scale.NewParameters(ctx, krc.Parameters, flags.Params)
diff --git a/pkg/kwokctl/runtime/cluster.go b/pkg/kwokctl/runtime/cluster.go
index de62e108..f8e908b4 100644
--- a/pkg/kwokctl/runtime/cluster.go
+++ b/pkg/kwokctl/runtime/cluster.go
@@ -100,11 +100,10 @@ func (c *Cluster) Config(ctx context.Context) (*internalversion.KwokctlConfigura
 	}
 	c.conf = conf
 	logger := log.FromContext(ctx)
+	logger = logger.With("kwokctlVersion", consts.Version)
 	if conf.Status.Version == "" {
-		logger.Warn("The cluster was created by an older version of kwokctl, "+
+		logger.Warn("The cluster was created by a older version of kwokctl, " +
 			"please recreate the cluster",
-			"createdByVersion", "<0.3.0",
-			"kwokctlVersion", consts.Version,
 		)
 		conf.Status.Version = "0.0.0"
 	} else if conf.Status.Version != consts.Version {
@@ -121,13 +120,11 @@ func (c *Cluster) Config(ctx context.Context) (*internalversion.KwokctlConfigura
 			logger.Warn("The cluster was created by a older version of kwokctl, "+
 				"please recreate the cluster",
 				"createdByVersion", conf.Status.Version,
-				"kwokctlVersion", consts.Version,
 			)
 		case currentVer.GT(ver):
 			logger.Warn("The cluster was created by a newer version of kwokctl, "+
 				"please upgrade kwokctl or recreate the cluster",
 				"createdByVersion", conf.Status.Version,
-				"kwokctlVersion", consts.Version,
 			)
 		}
 	}
@@ -182,8 +179,7 @@ func (c *Cluster) Save(ctx context.Context) error {
 	}
 
 	if conf.Status.Version == "" {
-		c.conf = c.conf.DeepCopy()
-		c.conf.Status.Version = consts.Version
+		conf.Status.Version = consts.Version
 	}
 
 	others := config.FilterWithoutTypeFromContext[*internalversion.KwokctlConfiguration](ctx)
@@ -207,29 +203,22 @@ func (c *Cluster) Save(ctx context.Context) error {
 func (c *Cluster) getDefaultStages(updateFrequency int64, lease bool) ([]config.InternalObject, error) {
 	objs := []config.InternalObject{}
 
-	nodeInit, err := config.Unmarshal([]byte(nodefast.DefaultNodeInit))
+	nodeInitStage, err := config.UnmarshalWithType[*internalversion.Stage](nodefast.DefaultNodeInit)
 	if err != nil {
 		return nil, err
 	}
-	if _, ok := nodeInit.(*internalversion.Stage); !ok {
-		return nil, fmt.Errorf("failed to get node init stage %T", nodeInit)
-	}
-	objs = append(objs, nodeInit)
+	objs = append(objs, nodeInitStage)
 
 	rawHeartbeat := nodeheartbeat.DefaultNodeHeartbeat
 	if lease {
 		rawHeartbeat = nodeheartbeatwithlease.DefaultNodeHeartbeatWithLease
 	}
 
-	nodeHeartbeat, err := config.Unmarshal([]byte(rawHeartbeat))
+	nodeHeartbeatStage, err := config.UnmarshalWithType[*internalversion.Stage](rawHeartbeat)
 	if err != nil {
 		return nil, err
 	}
 
-	nodeHeartbeatStage, ok := nodeHeartbeat.(*internalversion.Stage)
-	if !ok {
-		return nil, fmt.Errorf("failed to get node nodeheartbeat stage %T", nodeHeartbeat)
-	}
 	if updateFrequency > 0 {
 		durationMilliseconds := format.ElemOrDefault(nodeHeartbeatStage.Spec.Delay.DurationMilliseconds)
 		jitterDurationMilliseconds := format.ElemOrDefault(nodeHeartbeatStage.Spec.Delay.JitterDurationMilliseconds)
@@ -286,7 +275,8 @@ func (c *Cluster) Ready(ctx context.Context) (bool, error) {
 	if !bytes.Equal(out.Bytes(), []byte("ok")) {
 		logger := log.FromContext(ctx)
 		logger.Debug("Check Ready",
-			"method", "get /healthz",
+			"method", "get",
+			"path", "/healthz",
 			"response", out,
 		)
 		return false, nil
diff --git a/pkg/kwokctl/runtime/compose/cluster.go b/pkg/kwokctl/runtime/compose/cluster.go
index 0b21c2bd..df56a4e1 100644
--- a/pkg/kwokctl/runtime/compose/cluster.go
+++ b/pkg/kwokctl/runtime/compose/cluster.go
@@ -25,8 +25,6 @@ import (
 	"strings"
 	"time"
 
-	"sigs.k8s.io/yaml"
-
 	"sigs.k8s.io/kwok/pkg/consts"
 	"sigs.k8s.io/kwok/pkg/kwokctl/components"
 	"sigs.k8s.io/kwok/pkg/kwokctl/dryrun"
@@ -41,6 +39,7 @@ import (
 	"sigs.k8s.io/kwok/pkg/utils/net"
 	"sigs.k8s.io/kwok/pkg/utils/path"
 	"sigs.k8s.io/kwok/pkg/utils/wait"
+	"sigs.k8s.io/kwok/pkg/utils/yaml"
 )
 
 // Cluster is an implementation of Runtime for docker.
diff --git a/pkg/kwokctl/snapshot/load.go b/pkg/kwokctl/snapshot/load.go
index 93bb3a79..b364f8ac 100644
--- a/pkg/kwokctl/snapshot/load.go
+++ b/pkg/kwokctl/snapshot/load.go
@@ -118,7 +118,7 @@ func (l *loader) Load(ctx context.Context, r io.Reader) error {
 	start := time.Now()
 	decoder := yaml.NewDecoder(r)
 
-	err := decoder.Decode(func(obj *unstructured.Unstructured) error {
+	err := decoder.DecodeToUnstructured(func(obj *unstructured.Unstructured) error {
 		if err := ctx.Err(); err != nil {
 			return err
 		}
diff --git a/pkg/log/logger.go b/pkg/log/logger.go
index 55ceae96..951488d7 100644
--- a/pkg/log/logger.go
+++ b/pkg/log/logger.go
@@ -23,8 +23,7 @@ import (
 	"os"
 	"time"
 
-	//nolint:depguard
-	"golang.org/x/exp/slog"
+	"golang.org/x/exp/slog" //nolint:depguard
 	"golang.org/x/term"
 )
 
diff --git a/pkg/log/logger_ctl.go b/pkg/log/logger_ctl.go
index 7a074b85..ec2e2267 100644
--- a/pkg/log/logger_ctl.go
+++ b/pkg/log/logger_ctl.go
@@ -27,8 +27,7 @@ import (
 	"unicode/utf8"
 
 	"github.com/wzshiming/ctc"
-	//nolint:depguard
-	"golang.org/x/exp/slog"
+	"golang.org/x/exp/slog" //nolint:depguard
 	"golang.org/x/term"
 
 	"sigs.k8s.io/kwok/pkg/utils/format"
diff --git a/pkg/log/logger_noop.go b/pkg/log/logger_noop.go
index 57b104c8..013a779f 100644
--- a/pkg/log/logger_noop.go
+++ b/pkg/log/logger_noop.go
@@ -19,8 +19,7 @@ package log
 import (
 	"context"
 
-	//nolint:depguard
-	"golang.org/x/exp/slog"
+	"golang.org/x/exp/slog" //nolint:depguard
 )
 
 var noop = wrapSlog(noopHandler{}, LevelInfo)
diff --git a/pkg/log/wrapper.go b/pkg/log/wrapper.go
index 9a152d18..e2874cf7 100644
--- a/pkg/log/wrapper.go
+++ b/pkg/log/wrapper.go
@@ -24,8 +24,7 @@ import (
 	"strings"
 	"time"
 
-	//nolint:depguard
-	"golang.org/x/exp/slog"
+	"golang.org/x/exp/slog" //nolint:depguard
 )
 
 // Level is the logging level.
diff --git a/pkg/utils/gotpl/renderer.go b/pkg/utils/gotpl/renderer.go
index 880415b9..a347ad0f 100644
--- a/pkg/utils/gotpl/renderer.go
+++ b/pkg/utils/gotpl/renderer.go
@@ -23,11 +23,10 @@ import (
 	"strings"
 	"text/template"
 
-	"sigs.k8s.io/yaml"
-
 	"sigs.k8s.io/kwok/pkg/utils/maps"
 	"sigs.k8s.io/kwok/pkg/utils/pools"
 	"sigs.k8s.io/kwok/pkg/utils/slices"
+	"sigs.k8s.io/kwok/pkg/utils/yaml"
 )
 
 // FuncMap is a map of functions that can be used in templates.
diff --git a/pkg/utils/maps/maps.go b/pkg/utils/maps/maps.go
new file mode 100644
index 00000000..ca50c864
--- /dev/null
+++ b/pkg/utils/maps/maps.go
@@ -0,0 +1,56 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package maps
+
+// Keys returns the keys of the map m.
+// The keys will be in an indeterminate order.
+func Keys[M ~map[K]V, K comparable, V any](m M) []K {
+	r := make([]K, 0, len(m))
+	for k := range m {
+		r = append(r, k)
+	}
+	return r
+}
+
+// Values returns the values of the map m.
+// The values will be in an indeterminate order.
+func Values[M ~map[K]V, K comparable, V any](m M) []V {
+	r := make([]V, 0, len(m))
+	for _, v := range m {
+		r = append(r, v)
+	}
+	return r
+}
+
+// Merge merges the maps in m into a single map.
+// If a key is present in multiple maps, the value from the last map will be used.
+func Merge[M ~map[K]V, K comparable, V any](m ...M) M {
+	if len(m) == 0 {
+		return nil
+	}
+	if len(m) == 1 {
+		return m[0]
+	}
+
+	r := make(M)
+	for _, m := range m {
+		for k, v := range m {
+			r[k] = v
+		}
+	}
+	return r
+}
diff --git a/pkg/utils/wait/wait.go b/pkg/utils/wait/wait.go
index 80fe1c8d..8f16d487 100644
--- a/pkg/utils/wait/wait.go
+++ b/pkg/utils/wait/wait.go
@@ -20,8 +20,7 @@ import (
 	"context"
 	"time"
 
-	//nolint:depguard
-	"k8s.io/apimachinery/pkg/util/wait"
+	"k8s.io/apimachinery/pkg/util/wait" //nolint:depguard
 )
 
 const (
diff --git a/pkg/utils/yaml/decoder.go b/pkg/utils/yaml/decoder.go
index d169b535..4cabcf63 100644
--- a/pkg/utils/yaml/decoder.go
+++ b/pkg/utils/yaml/decoder.go
@@ -20,51 +20,39 @@ import (
 	"errors"
 	"io"
 
-	yamlv3 "gopkg.in/yaml.v3"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
-	"sigs.k8s.io/yaml"
+	"k8s.io/apimachinery/pkg/util/yaml" //nolint:depguard
 )
 
 // Decoder is a YAML decoder.
 type Decoder struct {
-	decoder *yamlv3.Decoder
+	decoder *yaml.YAMLToJSONDecoder
 }
 
 // NewDecoder returns a new YAML decoder.
 func NewDecoder(r io.Reader) *Decoder {
 	return &Decoder{
-		decoder: yamlv3.NewDecoder(r),
+		decoder: yaml.NewYAMLToJSONDecoder(r),
 	}
 }
 
-// Decode decodes YAML into a list of unstructured objects.
-func (d *Decoder) Decode(visitFunc func(obj *unstructured.Unstructured) error) error {
-	var tmp map[string]interface{}
+// Decode decodes YAML into an object.
+func (d *Decoder) Decode(obj any) error {
+	return d.decoder.Decode(obj)
+}
+
+// DecodeToUnstructured decodes YAML into a list of unstructured objects.
+func (d *Decoder) DecodeToUnstructured(visitFunc func(obj *unstructured.Unstructured) error) error {
 	for {
-		err := d.decoder.Decode(&tmp)
+		obj := &unstructured.Unstructured{}
+		err := d.Decode(obj)
 		if err != nil {
 			if errors.Is(err, io.EOF) {
 				return nil
 			}
 			return err
 		}
-		if len(tmp) == 0 {
-			continue
-		}
-		data, err := yamlv3.Marshal(tmp)
-		if err != nil {
-			return err
-		}
-		data, err = yaml.YAMLToJSON(data)
-		if err != nil {
-			return err
-		}
-		obj := &unstructured.Unstructured{}
-		err = obj.UnmarshalJSON(data)
-		if err != nil {
-			return err
-		}
 
 		if obj.IsList() {
 			err = obj.EachListItem(func(object runtime.Object) error {
diff --git a/pkg/utils/yaml/encoder.go b/pkg/utils/yaml/encoder.go
index 627f0d3b..1879372e 100644
--- a/pkg/utils/yaml/encoder.go
+++ b/pkg/utils/yaml/encoder.go
@@ -21,7 +21,6 @@ import (
 	"sync/atomic"
 
 	"k8s.io/apimachinery/pkg/runtime"
-	"sigs.k8s.io/yaml"
 )
 
 // Encoder is a YAML encoder.
@@ -48,7 +47,7 @@ func (p *Encoder) Encode(obj runtime.Object) error {
 		}
 	}
 
-	output, err := yaml.Marshal(obj)
+	output, err := Marshal(obj)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/utils/maps/ref.go b/pkg/utils/yaml/ref.go
similarity index 70%
rename from pkg/utils/maps/ref.go
rename to pkg/utils/yaml/ref.go
index 2d9bd5fa..80cae304 100644
--- a/pkg/utils/maps/ref.go
+++ b/pkg/utils/yaml/ref.go
@@ -14,13 +14,16 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package maps
+package yaml
 
 import (
-	"golang.org/x/exp/maps"
+	"sigs.k8s.io/yaml" //nolint:depguard
 )
 
-// Keys returns the keys of the map.
-func Keys[M ~map[K]V, K comparable, V any](m M) []K {
-	return maps.Keys(m)
-}
+// The following functions are exported for testing purposes only.
+var (
+	YAMLToJSON = yaml.YAMLToJSON
+	JSONToYAML = yaml.JSONToYAML
+	Unmarshal  = yaml.Unmarshal
+	Marshal    = yaml.Marshal
+)
diff --git a/site/content/en/docs/generated/kwokctl.md b/site/content/en/docs/generated/kwokctl.md
index 37060b50..da3f33e1 100644
--- a/site/content/en/docs/generated/kwokctl.md
+++ b/site/content/en/docs/generated/kwokctl.md
@@ -25,7 +25,7 @@ kwokctl [command] [flags]
 * [kwokctl export](kwokctl_export.md)	 - Exports one of [logs]
 * [kwokctl get](kwokctl_get.md)	 - Gets one of [artifacts, clusters, kubeconfig]
 * [kwokctl kubectl](kwokctl_kubectl.md)	 - kubectl in cluster
-* [kwokctl logs](kwokctl_logs.md)	 - Logs one of [audit, etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kwok-controller, prometheus]
+* [kwokctl logs](kwokctl_logs.md)	 - Logs one of [audit, etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kwok-controller, dashboard, prometheus, jaeger]
 * [kwokctl scale](kwokctl_scale.md)	 - Scale a resource in cluster
 * [kwokctl snapshot](kwokctl_snapshot.md)	 - Snapshot [save, restore, export] one of cluster
 * [kwokctl start](kwokctl_start.md)	 - Start one of [cluster]
diff --git a/site/content/en/docs/generated/kwokctl_logs.md b/site/content/en/docs/generated/kwokctl_logs.md
index 77d4aa7a..f65e2979 100644
--- a/site/content/en/docs/generated/kwokctl_logs.md
+++ b/site/content/en/docs/generated/kwokctl_logs.md
@@ -1,6 +1,6 @@
 ## kwokctl logs
 
-Logs one of [audit, etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kwok-controller, prometheus]
+Logs one of [audit, etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kwok-controller, dashboard, prometheus, jaeger]
 
 ```
 kwokctl logs [command] [flags]


Test patch:
diff --git a/pkg/kwok/controllers/controller_test.go b/pkg/kwok/controllers/controller_test.go
index 441dc312..627388a6 100644
--- a/pkg/kwok/controllers/controller_test.go
+++ b/pkg/kwok/controllers/controller_test.go
@@ -71,18 +71,18 @@ func TestController(t *testing.T) {
 		},
 	}
 
-	nodeInit, _ := config.Unmarshal([]byte(nodefast.DefaultNodeInit))
-	nodeStages := []*internalversion.Stage{nodeInit.(*internalversion.Stage)}
+	nodeInit, _ := config.UnmarshalWithType[*internalversion.Stage](nodefast.DefaultNodeInit)
+	nodeStages := []*internalversion.Stage{nodeInit}
 	podStages, _ := slices.MapWithError([]string{
 		podfast.DefaultPodReady,
 		podfast.DefaultPodComplete,
 		podfast.DefaultPodDelete,
 	}, func(s string) (*internalversion.Stage, error) {
-		iobj, err := config.Unmarshal([]byte(s))
+		stage, err := config.UnmarshalWithType[*internalversion.Stage](s)
 		if err != nil {
 			return nil, err
 		}
-		return iobj.(*internalversion.Stage), nil
+		return stage, nil
 	})
 
 	tests := []struct {
diff --git a/pkg/kwok/controllers/node_controller_test.go b/pkg/kwok/controllers/node_controller_test.go
index a0688fe1..a35e298b 100644
--- a/pkg/kwok/controllers/node_controller_test.go
+++ b/pkg/kwok/controllers/node_controller_test.go
@@ -75,8 +75,8 @@ func TestNodeController(t *testing.T) {
 		return node.Annotations["node"] == "true"
 	}
 
-	nodeInit, _ := config.Unmarshal([]byte(nodefast.DefaultNodeInit))
-	nodeStages := []*internalversion.Stage{nodeInit.(*internalversion.Stage)}
+	nodeInit, _ := config.UnmarshalWithType[*internalversion.Stage](nodefast.DefaultNodeInit)
+	nodeStages := []*internalversion.Stage{nodeInit}
 
 	lifecycle, _ := NewLifecycle(nodeStages)
 	nodes, err := NewNodeController(NodeControllerConfig{
diff --git a/pkg/kwok/controllers/pod_controller_test.go b/pkg/kwok/controllers/pod_controller_test.go
index 953d2830..2af47303 100644
--- a/pkg/kwok/controllers/pod_controller_test.go
+++ b/pkg/kwok/controllers/pod_controller_test.go
@@ -172,13 +172,7 @@ func TestPodController(t *testing.T) {
 		podfast.DefaultPodReady,
 		podfast.DefaultPodComplete,
 		podfast.DefaultPodDelete,
-	}, func(s string) (*internalversion.Stage, error) {
-		iobj, err := config.Unmarshal([]byte(s))
-		if err != nil {
-			return nil, err
-		}
-		return iobj.(*internalversion.Stage), nil
-	})
+	}, config.UnmarshalWithType[*internalversion.Stage, string])
 
 	ctx := context.Background()
 	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.LevelDebug))
diff --git a/pkg/log/logger_ctl_test.go b/pkg/log/logger_ctl_test.go
index 2d0d3da5..aa2c6f0b 100644
--- a/pkg/log/logger_ctl_test.go
+++ b/pkg/log/logger_ctl_test.go
@@ -21,8 +21,7 @@ import (
 	"sort"
 	"testing"
 
-	//nolint:depguard
-	"golang.org/x/exp/slog"
+	"golang.org/x/exp/slog" //nolint:depguard
 )
 
 func Test_quoteRangeTable(t *testing.T) {
diff --git a/pkg/utils/yaml/decoder_test.go b/pkg/utils/yaml/decoder_test.go
index 904f8387..b98966c7 100644
--- a/pkg/utils/yaml/decoder_test.go
+++ b/pkg/utils/yaml/decoder_test.go
@@ -29,7 +29,7 @@ import (
 	"sigs.k8s.io/kwok/pkg/utils/slices"
 )
 
-func TestDecoder(t *testing.T) {
+func TestDecodeToUnstructured(t *testing.T) {
 	tests := []struct {
 		name    string
 		data    string
@@ -88,12 +88,12 @@ spec:
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			got := []*unstructured.Unstructured{}
-			err := NewDecoder(bytes.NewBufferString(tt.data)).Decode(func(obj *unstructured.Unstructured) error {
+			err := NewDecoder(bytes.NewBufferString(tt.data)).DecodeToUnstructured(func(obj *unstructured.Unstructured) error {
 				got = append(got, obj)
 				return nil
 			})
 			if (err != nil) != tt.wantErr {
-				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("DecodeToUnstructured() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}