Repository: kubernetes-sigs/mcs-api Branch: master Commit: 4546fabf7251 Files: 138 Total size: 476.5 KB Directory structure: gitextract_e338xpw6/ ├── .github/ │ └── workflows/ │ └── auto-label.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── RELEASE.md ├── SECURITY.md ├── SECURITY_CONTACTS ├── code-of-conduct.md ├── config/ │ ├── crd/ │ │ ├── embed.go │ │ ├── multicluster.x-k8s.io_serviceexports.yaml │ │ └── multicluster.x-k8s.io_serviceimports.yaml │ ├── crd-base/ │ │ ├── multicluster.x-k8s.io_serviceexports.yaml │ │ └── multicluster.x-k8s.io_serviceimports.yaml │ └── rbac/ │ └── role.yaml ├── conformance/ │ ├── clusterip_service_dns.go │ ├── conformance_suite.go │ ├── conformance_suite_test.go │ ├── connectivity.go │ ├── endpoint_slice.go │ ├── framework.go │ ├── go.mod │ ├── go.sum │ ├── headless_service_dns.go │ ├── k8s_objects.go │ ├── report.go │ ├── report_template.gohtml │ └── service_import.go ├── controllers/ │ ├── cmd/ │ │ └── servicecontroller/ │ │ └── servicecontroller.go │ ├── common.go │ ├── controllers_suite_test.go │ ├── endpointslice.go │ ├── endpointslice_test.go │ ├── go.mod │ ├── go.sum │ ├── service.go │ ├── serviceimport.go │ └── serviceimport_test.go ├── demo/ │ ├── .gitignore │ ├── demo.sh │ ├── edit-meta │ ├── reset.sh │ ├── udemo.sh │ └── yaml/ │ ├── dep1.yaml │ ├── dep2.yaml │ ├── serviceimport-with-vip.yaml │ ├── serviceimport.yaml │ └── svc.yaml ├── e2e/ │ ├── connectivity_test.go │ ├── e2e_suite_test.go │ ├── go.mod │ ├── go.sum │ └── localserviceimpact_test.go ├── go.mod ├── go.sum ├── hack/ │ ├── boilerplate/ │ │ ├── boilerplate.go.txt │ │ ├── boilerplate.py │ │ ├── boilerplate.py.txt │ │ └── boilerplate.sh.txt │ ├── boilerplate.go.txt │ ├── kube-env.sh │ ├── update-codegen.sh │ ├── update-k8s.sh │ ├── verify-all.sh │ ├── verify-boilerplate.sh │ ├── verify-codegen.sh │ ├── verify-crd-bump-revision.sh │ ├── verify-crds.sh │ ├── verify-gofmt.sh │ └── verify-golint.sh ├── pkg/ │ ├── apis/ │ │ ├── v1alpha1/ │ │ │ ├── BUILD │ │ │ ├── doc.go │ │ │ ├── serviceexport.go │ │ │ ├── serviceimport.go │ │ │ ├── well_known_labels.go │ │ │ ├── zz_generated.deepcopy.go │ │ │ └── zz_generated.register.go │ │ └── v1beta1/ │ │ ├── BUILD │ │ ├── doc.go │ │ ├── serviceexport.go │ │ ├── serviceimport.go │ │ ├── well_known_labels.go │ │ ├── zz_generated.deepcopy.go │ │ └── zz_generated.register.go │ └── client/ │ ├── clientset/ │ │ └── versioned/ │ │ ├── clientset.go │ │ ├── fake/ │ │ │ ├── clientset_generated.go │ │ │ ├── doc.go │ │ │ └── register.go │ │ ├── scheme/ │ │ │ ├── doc.go │ │ │ └── register.go │ │ └── typed/ │ │ └── apis/ │ │ ├── v1alpha1/ │ │ │ ├── apis_client.go │ │ │ ├── doc.go │ │ │ ├── fake/ │ │ │ │ ├── doc.go │ │ │ │ ├── fake_apis_client.go │ │ │ │ ├── fake_serviceexport.go │ │ │ │ └── fake_serviceimport.go │ │ │ ├── generated_expansion.go │ │ │ ├── serviceexport.go │ │ │ └── serviceimport.go │ │ └── v1beta1/ │ │ ├── apis_client.go │ │ ├── doc.go │ │ ├── fake/ │ │ │ ├── doc.go │ │ │ ├── fake_apis_client.go │ │ │ ├── fake_serviceexport.go │ │ │ └── fake_serviceimport.go │ │ ├── generated_expansion.go │ │ ├── serviceexport.go │ │ └── serviceimport.go │ ├── informers/ │ │ └── externalversions/ │ │ ├── apis/ │ │ │ ├── interface.go │ │ │ ├── v1alpha1/ │ │ │ │ ├── interface.go │ │ │ │ ├── serviceexport.go │ │ │ │ └── serviceimport.go │ │ │ └── v1beta1/ │ │ │ ├── interface.go │ │ │ ├── serviceexport.go │ │ │ └── serviceimport.go │ │ ├── factory.go │ │ ├── generic.go │ │ └── internalinterfaces/ │ │ └── factory_interfaces.go │ └── listers/ │ └── apis/ │ ├── v1alpha1/ │ │ ├── expansion_generated.go │ │ ├── serviceexport.go │ │ └── serviceimport.go │ └── v1beta1/ │ ├── expansion_generated.go │ ├── serviceexport.go │ └── serviceimport.go ├── scripts/ │ ├── .gitignore │ ├── c1.yaml │ ├── c2.yaml │ ├── coredns-rbac.json │ ├── down.sh │ ├── e2e-test.sh │ ├── up.sh │ └── util.sh └── tools/ ├── go.mod ├── go.sum └── tools.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/auto-label.yml ================================================ --- name: Label issues on: issues: types: - opened - reopened jobs: label_issues: runs-on: ubuntu-latest permissions: issues: write steps: - run: gh issue edit "$NUMBER" --add-label "$LABELS" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} NUMBER: ${{ github.event.issue.number }} LABELS: sig/multicluster ================================================ FILE: .gitignore ================================================ *~ .\#* ._* \#*\# /_artifacts/ /bazel-* bin .classpath /cluster /.config/gcloud*/ *.dll /doc_tmp/ !\.drone\.sec .DS_Store *.dylib .envrc *.exe *.exe~ /_gopath/ /.gsutil/ /hack/.test-cmd-auth **/.hg* .idea/ *.iml /junit*.xml /.make/ .netrwhist network_closure.sh *.out /_output*/ /output*/ .project *.pyc [._]*.s[a-w][a-z] [._]s[a-w][a-z] Session.vim .settings/** *.so *.swo *.swp .tags* *.test /third_party/pkg .*.timestamp /_tmp/ *.un~ .vagrant !vendor/**/zz_generated.* .vscode /www/test_out report.html report.yaml .*.timestamp ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ ## Getting Started We have full documentation on how to get started contributing here: - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests - [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](https://git.k8s.io/community/contributors/guide#contributing) - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers ## Mentorship - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! ## Contact Information - [Slack](https://kubernetes.slack.com/messages/sig-service-catalog) - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-service-catalog) ================================================ FILE: Dockerfile ================================================ # Build the manager binary FROM golang:1.23 as builder WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum RUN go mod download # Copy the go source COPY controllers/ controllers/ RUN go -C controllers mod download COPY pkg/ pkg/ # Build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go -C controllers build -a -o /workspace/controller cmd/servicecontroller/servicecontroller.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/controller . USER nonroot:nonroot ENTRYPOINT ["/controller"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} 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. ================================================ FILE: Makefile ================================================ # Copyright 2019 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. DOCKER ?= docker # TOP is the current directory where this Makefile lives. TOP := $(dir $(firstword $(MAKEFILE_LIST))) # ROOT is the root of the mkdocs tree. ROOT := $(abspath $(TOP)) # Image URL to use all building/pushing image targets IMG ?= mcs-api-controller:latest # Need v1 to support defaults in CRDs, unfortunately limiting us to k8s 1.16+ CRD_OPTIONS ?= "crd:crdVersions=v1" CONTROLLER_GEN=go -C tools run sigs.k8s.io/controller-tools/cmd/controller-gen # enable Go modules export GO111MODULE=on .PHONY: all all: generate manifests controller verify .PHONY: e2e-test e2e-test: export MCS_CONTROLLER_IMAGE := $(IMG) e2e-test: docker-build ./scripts/e2e-test.sh .PHONY: demo demo: export MCS_CONTROLLER_IMAGE := $(IMG) demo: docker-build ./scripts/up.sh ./demo/demo.sh ./scripts/down.sh # Build controller binary .PHONY: controller controller: generate fmt vet go -C controllers build -o $(ROOT)/bin/manager cmd/servicecontroller/servicecontroller.go # Run go fmt against code .PHONY: fmt fmt: for m in . conformance controllers e2e; do go -C $$m fmt ./...; done # Run go vet against code .PHONY: vet vet: for m in . conformance controllers e2e; do go -C $$m vet ./...; done # Run generators for Deepcopy funcs and CRDs .PHONY: generate generate: ./hack/update-codegen.sh $(CONTROLLER_GEN) object:headerFile=$(ROOT)/hack/boilerplate.go.txt paths="$(ROOT)/..." # Generate manifests e.g. CRD, RBAC etc. .PHONY: manifests manifests: $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=mcs-derived-service-manager output:rbac:dir="$(ROOT)/config/rbac" webhook schemapatch:manifests="$(ROOT)/config/crd-base" paths="$(ROOT)/..." output:crd:none output:schemapatch:dir="$(ROOT)/config/crd" # Run tests .PHONY: test test: generate fmt vet manifests for m in . controllers; do go -C $$m test ./... -coverprofile cover.out; done # Install CRD's and example resources to a pre-existing cluster. .PHONY: install install: manifests crd # Remove installed CRD's and CR's. .PHONY: uninstall uninstall: ./hack/delete-crds.sh # Run static analysis. .PHONY: verify verify: ./hack/verify-all.sh -v # Build docker containers .PHONY: docker-build docker-build: generate fmt vet manifests docker build . -t ${IMG} # Push the docker image .PHONY: docker-push docker-push: docker-build docker push ${IMG} # Run against the configured Kubernetes cluster in ~/.kube/config run: generate fmt vet manifests go run ./cmd/servicecontroller/servicecontroller.go ================================================ FILE: OWNERS ================================================ # See the OWNERS docs at https://go.k8s.io/owners reviewers: - jeremyot - lauralorenz - skitt - MrFreezeex - munnerz - RainbowMango - ryanzhang-oss # Pending org membership # - jnpacker approvers: - JeremyOT - lauralorenz - skitt emeritus_approvers: - pmorie labels: - sig/multicluster ================================================ FILE: README.md ================================================ # Multi-cluster Service APIs This repository hosts the Multi-Cluster Service APIs. Providers can import packages in this repo to ensure their multi-cluster service controller implementations will be compatible with MCS data planes. This repo contains the initial implementation according to [KEP-1645][kep]. [kep]: https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api ## Try it out To see the API in action, run `make demo` to build and run a local demo against a pair of kind clusters. Alternatively, you can take a self guided tour. Use: - `./scripts/up.sh` to create a pair of clusters with mutually connected networks and install the `mcs-api-controller`. _This will use a pre-existing controller image if available, it's recommended to run `make docker-build` first._ - `./demo/demo.sh` to run the same demo as above against your newly created clusters (must run `./scripts/up.sh` first). - `./scripts/down.sh` to tear down your clusters. ## Community, discussion, contribution, and support Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). You can reach the maintainers of this project at: - [Slack](https://kubernetes.slack.com/messages/sig-multicluster) - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-multicluster) [Our meeting schedule is here]( https://github.com/kubernetes/community/tree/master/sig-multicluster#meetings) ## Technical Leads - @pmorie - @jeremyot ### Code of conduct Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). ================================================ FILE: RELEASE.md ================================================ # Release Process The Kubernetes Template Project is released on an as-needed basis. The process is as follows: 1. An issue is proposing a new release with a changelog since the last release 1. All [OWNERS](OWNERS) must LGTM this release 1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` 1. The release issue is closed 1. An announcement email is sent to `kubernetes-dev@googlegroups.com` with the subject `[ANNOUNCE] kubernetes-template-project $VERSION is released` ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Security Announcements Join the [kubernetes-security-announce] group for security and vulnerability announcements. You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss]. ## Reporting a Vulnerability Instructions for reporting a vulnerability can be found on the [Kubernetes Security and Disclosure Information] page. ## Supported Versions Information about supported Kubernetes versions can be found on the [Kubernetes version and version skew support policy] page on the Kubernetes website. [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability ================================================ FILE: SECURITY_CONTACTS ================================================ # Defined below are the security contacts for this repo. # # They are the contact point for the Product Security Committee to reach out # to for triaging and handling of incoming issues. # # The below names agree to abide by the # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) # and will be removed and replaced if they violate that agreement. # # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # INSTRUCTIONS AT https://kubernetes.io/security/ pmorie JeremyOT ================================================ FILE: code-of-conduct.md ================================================ # Kubernetes Community Code of Conduct Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) ================================================ FILE: config/crd/embed.go ================================================ /* Copyright 2025 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 crd import _ "embed" // import embed to be able to use go:embed var ( // ServiceExportCRD is the embedded YAML for the ServiceExport CRD //go:embed multicluster.x-k8s.io_serviceexports.yaml ServiceExportCRD []byte // ServiceImportCRD is the embedded YAML for the ServiceImport CRD //go:embed multicluster.x-k8s.io_serviceimports.yaml ServiceImportCRD []byte ) const ( // ReleaseVersionLabel is the label which indicate the release version ReleaseVersionLabel = "multicluster.x-k8s.io/release-version" // CustomResourceDefinitionSchemaRevisionLabel is the label which holds the CRD schema revision CustomResourceDefinitionSchemaRevisionLabel = "multicluster.x-k8s.io/crd-schema-revision" ) ================================================ FILE: config/crd/multicluster.x-k8s.io_serviceexports.yaml ================================================ # Copyright 2020 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. apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: serviceexports.multicluster.x-k8s.io labels: multicluster.x-k8s.io/release-version: "v0.5.0" # The revision is updated on each CRD change and reset back to 0 on every new version. # It can be used together with the version label when installing those CRDs # and prevent any downgrades. multicluster.x-k8s.io/crd-schema-revision: "0" spec: group: multicluster.x-k8s.io scope: Namespaced names: plural: serviceexports singular: serviceexport kind: ServiceExport shortNames: - svcex - svcexport versions: - name: v1alpha1 served: true storage: false subresources: status: {} additionalPrinterColumns: - name: Age type: date jsonPath: .metadata.creationTimestamp "schema": "openAPIV3Schema": description: |- ServiceExport declares that the Service with the same name and namespace as this export should be consumable from other clusters. type: object properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: spec defines the behavior of a ServiceExport. type: object properties: exportedAnnotations: description: exportedAnnotations describes the annotations exported. It is optional for implementation. type: object additionalProperties: type: string exportedLabels: description: exportedLabels describes the labels exported. It is optional for implementation. type: object additionalProperties: type: string status: description: |- status describes the current state of an exported service. Service configuration comes from the Service that had the same name and namespace as this ServiceExport. Populated by the multi-cluster service implementation's controller. type: object properties: conditions: type: array items: description: Condition contains details for one aspect of the current state of this API Resource. type: object required: - lastTransitionTime - message - reason - status - type properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. type: string format: date-time message: description: |- message is a human readable message indicating details about the transition. This may be an empty string. type: string maxLength: 32768 observedGeneration: description: |- observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. type: integer format: int64 minimum: 0 reason: description: |- reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. type: string maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ status: description: status of the condition, one of True, False, Unknown. type: string enum: - "True" - "False" - Unknown type: description: type of condition in CamelCase or in foo.example.com/CamelCase. type: string maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map - name: v1beta1 served: true storage: true subresources: status: {} additionalPrinterColumns: - name: Age type: date jsonPath: .metadata.creationTimestamp "schema": "openAPIV3Schema": description: |- ServiceExport declares that the Service with the same name and namespace as this export should be consumable from other clusters. type: object properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: spec defines the behavior of a ServiceExport. type: object properties: exportedAnnotations: description: exportedAnnotations describes the annotations exported. It is optional for implementation. type: object additionalProperties: type: string exportedLabels: description: exportedLabels describes the labels exported. It is optional for implementation. type: object additionalProperties: type: string status: description: |- status describes the current state of an exported service. Service configuration comes from the Service that had the same name and namespace as this ServiceExport. Populated by the multi-cluster service implementation's controller. type: object properties: conditions: type: array items: description: Condition contains details for one aspect of the current state of this API Resource. type: object required: - lastTransitionTime - message - reason - status - type properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. type: string format: date-time message: description: |- message is a human readable message indicating details about the transition. This may be an empty string. type: string maxLength: 32768 observedGeneration: description: |- observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. type: integer format: int64 minimum: 0 reason: description: |- reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. type: string maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ status: description: status of the condition, one of True, False, Unknown. type: string enum: - "True" - "False" - Unknown type: description: type of condition in CamelCase or in foo.example.com/CamelCase. type: string maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map ================================================ FILE: config/crd/multicluster.x-k8s.io_serviceimports.yaml ================================================ # Copyright 2020 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. apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: serviceimports.multicluster.x-k8s.io labels: multicluster.x-k8s.io/release-version: "v0.5.0" # The revision is updated on each CRD change and reset back to 0 on every new version. # It can be used together with the version label when installing those CRDs # and prevent any downgrades. multicluster.x-k8s.io/crd-schema-revision: "0" spec: group: multicluster.x-k8s.io scope: Namespaced names: plural: serviceimports singular: serviceimport kind: ServiceImport shortNames: - svcim - svcimport versions: - name: v1alpha1 served: true storage: false subresources: status: {} additionalPrinterColumns: - name: Type type: string description: The type of this ServiceImport jsonPath: .spec.type - name: IP type: string description: The VIP for this ServiceImport jsonPath: .spec.ips - name: Age type: date jsonPath: .metadata.creationTimestamp "schema": "openAPIV3Schema": description: ServiceImport describes a service imported from clusters in a ClusterSet. type: object properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: spec defines the behavior of a ServiceImport. type: object required: - ports - type properties: internalTrafficPolicy: description: |- InternalTrafficPolicy describes how nodes distribute service traffic they receive on the ClusterIP. If set to "Local", the proxy will assume that pods only want to talk to endpoints of the service on the same node as the pod, dropping the traffic if there are no local endpoints. The default value, "Cluster", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features). type: string ipFamilies: description: IPFamilies identifies all the IPFamilies assigned for this ServiceImport. type: array maxItems: 2 items: description: |- IPFamily represents the IP Family (IPv4 or IPv6). This type is used to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). type: string ips: description: ip will be used as the VIP for this service when type is ClusterSetIP. type: array maxItems: 2 items: type: string ports: type: array items: description: ServicePort represents the port on which the service is exposed type: object required: - port properties: appProtocol: description: |- The application protocol for this port. This is used as a hint for implementations to offer richer behavior for protocols that they understand. This field follows standard Kubernetes label syntax. Valid values are either: * Un-prefixed protocol names - reserved for IANA standard service names (as per RFC-6335 and https://www.iana.org/assignments/service-names). * Kubernetes-defined prefixed names: * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 * Other protocols should use implementation-defined prefixed names such as mycompany.com/my-custom-protocol. Field can be enabled with ServiceAppProtocol feature gate. type: string name: description: |- The name of this port within the service. This must be a DNS_LABEL. All ports within a ServiceSpec must have unique names. When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. Optional if only one ServicePort is defined on this service. type: string port: description: The port that will be exposed by this service. type: integer format: int32 protocol: description: |- The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". Default is TCP. type: string x-kubernetes-list-type: atomic sessionAffinity: description: |- Supports "ClientIP" and "None". Used to maintain session affinity. Enable client IP based session affinity. Must be ClientIP or None. Defaults to None. Ignored when type is Headless More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies type: string sessionAffinityConfig: description: sessionAffinityConfig contains session affinity configuration. type: object properties: clientIP: description: clientIP contains the configurations of Client IP based session affinity. type: object properties: timeoutSeconds: description: |- timeoutSeconds specifies the seconds of ClientIP type session sticky time. The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP". Default value is 10800(for 3 hours). type: integer format: int32 trafficDistribution: description: |- TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to "PreferClose", implementations should prioritize endpoints that are in the same zone. type: string type: description: |- type defines the type of this service. Must be ClusterSetIP or Headless. type: string enum: - ClusterSetIP - Headless status: description: |- status contains information about the exported services that form the multi-cluster service referenced by this ServiceImport. type: object properties: clusters: description: |- clusters is the list of exporting clusters from which this service was derived. type: array items: description: ClusterStatus contains service configuration mapped to a specific source cluster type: object required: - cluster properties: cluster: description: |- cluster is the name of the exporting cluster. Must be a valid RFC-1123 DNS label. type: string x-kubernetes-list-map-keys: - cluster x-kubernetes-list-type: map conditions: type: array items: description: Condition contains details for one aspect of the current state of this API Resource. type: object required: - lastTransitionTime - message - reason - status - type properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. type: string format: date-time message: description: |- message is a human readable message indicating details about the transition. This may be an empty string. type: string maxLength: 32768 observedGeneration: description: |- observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. type: integer format: int64 minimum: 0 reason: description: |- reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. type: string maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ status: description: status of the condition, one of True, False, Unknown. type: string enum: - "True" - "False" - Unknown type: description: type of condition in CamelCase or in foo.example.com/CamelCase. type: string maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map - name: v1beta1 served: true storage: true subresources: status: {} additionalPrinterColumns: - name: Type type: string description: The type of this ServiceImport jsonPath: .spec.type - name: IP type: string description: The VIP for this ServiceImport jsonPath: .spec.ips - name: Age type: date jsonPath: .metadata.creationTimestamp "schema": "openAPIV3Schema": description: ServiceImport describes a service imported from clusters in a ClusterSet. type: object properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: spec defines the behavior of a ServiceImport. type: object required: - ports - type properties: internalTrafficPolicy: description: |- InternalTrafficPolicy describes how nodes distribute service traffic they receive on the ClusterIP. If set to "Local", the proxy will assume that pods only want to talk to endpoints of the service on the same node as the pod, dropping the traffic if there are no local endpoints. The default value, "Cluster", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features). type: string ipFamilies: description: IPFamilies identifies all the IPFamilies assigned for this ServiceImport. type: array maxItems: 2 items: description: |- IPFamily represents the IP Family (IPv4 or IPv6). This type is used to express the family of an IP expressed by a type (e.g. service.spec.ipFamilies). type: string ips: description: ip will be used as the VIP for this service when type is ClusterSetIP. type: array maxItems: 2 items: type: string ports: type: array items: description: ServicePort represents the port on which the service is exposed type: object required: - port properties: appProtocol: description: |- The application protocol for this port. This is used as a hint for implementations to offer richer behavior for protocols that they understand. This field follows standard Kubernetes label syntax. Valid values are either: * Un-prefixed protocol names - reserved for IANA standard service names (as per RFC-6335 and https://www.iana.org/assignments/service-names). * Kubernetes-defined prefixed names: * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 * Other protocols should use implementation-defined prefixed names such as mycompany.com/my-custom-protocol. Field can be enabled with ServiceAppProtocol feature gate. type: string name: description: |- The name of this port within the service. This must be a DNS_LABEL. All ports within a ServiceSpec must have unique names. When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. Optional if only one ServicePort is defined on this service. type: string port: description: The port that will be exposed by this service. type: integer format: int32 protocol: description: |- The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". Default is TCP. type: string x-kubernetes-list-type: atomic sessionAffinity: description: |- Supports "ClientIP" and "None". Used to maintain session affinity. Enable client IP based session affinity. Must be ClientIP or None. Defaults to None. Ignored when type is Headless More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies type: string sessionAffinityConfig: description: sessionAffinityConfig contains session affinity configuration. type: object properties: clientIP: description: clientIP contains the configurations of Client IP based session affinity. type: object properties: timeoutSeconds: description: |- timeoutSeconds specifies the seconds of ClientIP type session sticky time. The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP". Default value is 10800(for 3 hours). type: integer format: int32 trafficDistribution: description: |- TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to "PreferClose", implementations should prioritize endpoints that are in the same zone. type: string type: description: |- type defines the type of this service. Must be ClusterSetIP or Headless. type: string enum: - ClusterSetIP - Headless status: description: |- status contains information about the exported services that form the multi-cluster service referenced by this ServiceImport. type: object properties: clusters: description: |- clusters is the list of exporting clusters from which this service was derived. type: array items: description: ClusterStatus contains service configuration mapped to a specific source cluster type: object required: - cluster properties: cluster: description: |- cluster is the name of the exporting cluster. Must be a valid RFC-1123 DNS label. type: string x-kubernetes-list-map-keys: - cluster x-kubernetes-list-type: map conditions: type: array items: description: Condition contains details for one aspect of the current state of this API Resource. type: object required: - lastTransitionTime - message - reason - status - type properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. type: string format: date-time message: description: |- message is a human readable message indicating details about the transition. This may be an empty string. type: string maxLength: 32768 observedGeneration: description: |- observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. type: integer format: int64 minimum: 0 reason: description: |- reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. type: string maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ status: description: status of the condition, one of True, False, Unknown. type: string enum: - "True" - "False" - Unknown type: description: type of condition in CamelCase or in foo.example.com/CamelCase. type: string maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map ================================================ FILE: config/crd-base/multicluster.x-k8s.io_serviceexports.yaml ================================================ # Copyright 2020 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. apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: serviceexports.multicluster.x-k8s.io labels: multicluster.x-k8s.io/release-version: "v0.5.0" # The revision is updated on each CRD change and reset back to 0 on every new version. # It can be used together with the version label when installing those CRDs # and prevent any downgrades. multicluster.x-k8s.io/crd-schema-revision: "0" spec: group: multicluster.x-k8s.io scope: Namespaced names: plural: serviceexports singular: serviceexport kind: ServiceExport shortNames: - svcex - svcexport versions: - name: v1alpha1 served: true storage: false subresources: status: {} additionalPrinterColumns: - name: Age type: date jsonPath: .metadata.creationTimestamp - name: v1beta1 served: true storage: true subresources: status: {} additionalPrinterColumns: - name: Age type: date jsonPath: .metadata.creationTimestamp ================================================ FILE: config/crd-base/multicluster.x-k8s.io_serviceimports.yaml ================================================ # Copyright 2020 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. apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: serviceimports.multicluster.x-k8s.io labels: multicluster.x-k8s.io/release-version: "v0.5.0" # The revision is updated on each CRD change and reset back to 0 on every new version. # It can be used together with the version label when installing those CRDs # and prevent any downgrades. multicluster.x-k8s.io/crd-schema-revision: "0" spec: group: multicluster.x-k8s.io scope: Namespaced names: plural: serviceimports singular: serviceimport kind: ServiceImport shortNames: - svcim - svcimport versions: - name: v1alpha1 served: true storage: false subresources: status: {} additionalPrinterColumns: - name: Type type: string description: The type of this ServiceImport jsonPath: .spec.type - name: IP type: string description: The VIP for this ServiceImport jsonPath: .spec.ips - name: Age type: date jsonPath: .metadata.creationTimestamp - name: v1beta1 served: true storage: true subresources: status: {} additionalPrinterColumns: - name: Type type: string description: The type of this ServiceImport jsonPath: .spec.type - name: IP type: string description: The VIP for this ServiceImport jsonPath: .spec.ips - name: Age type: date jsonPath: .metadata.creationTimestamp ================================================ FILE: config/rbac/role.yaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: mcs-derived-service-manager rules: - apiGroups: - "" resources: - services - services/status verbs: - create - get - list - patch - update - watch - apiGroups: - discovery.k8s.io resources: - endpointslices verbs: - get - list - patch - update - watch - apiGroups: - multicluster.x-k8s.io resources: - serviceimports verbs: - get - list - patch - update - watch ================================================ FILE: conformance/clusterip_service_dns.go ================================================ /* 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 conformance import ( "context" "fmt" "regexp" "strconv" "strings" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var _ = Describe("", Label(OptionalLabel, DNSLabel, ClusterIPLabel), func() { t := newTestDriver() Specify("A DNS lookup of the ..svc."+dnsDomain+" domain for a ClusterIP service should resolve to the "+ "clusterset IP", func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") for _, client := range clients { serviceImport := t.awaitServiceImport(ctx, &client, t.helloService.Name, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(serviceImport.Spec.IPs).ToNot(BeEmpty(), "ServiceImport on cluster %q does not contain an IP", client.name) }) By(fmt.Sprintf("Found ServiceImport on cluster %q with clusterset IPs %v", client.name, strings.Join(serviceImport.Spec.IPs, ","))) for _, clusterSetIP := range serviceImport.Spec.IPs { command := []string{"sh", "-c", fmt.Sprintf("nslookup -type=%s %s.%s.svc.%s.", dnsRecordTypeOf(ipFamilyOf(clusterSetIP)), t.helloService.Name, t.namespace, dnsDomain)} By(fmt.Sprintf("Executing %s command %q on cluster %q", ipFamilyOf(clusterSetIP), strings.Join(command, " "), client.name)) t.awaitCmdOutputMatches(&client, command, clusterSetIP, 1, reportNonConformant("")) } } }) Specify("A DNS SRV query of the ..svc."+dnsDomain+" domain for a ClusterIP service should return valid SRV "+ "records", func() { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") domainName := fmt.Sprintf("%s.%s.svc.%s", t.helloService.Name, t.namespace, dnsDomain) for _, client := range clients { expSRVRecs := []srvRecord{{ port: t.helloService.Spec.Ports[0].Port, domainName: domainName, }} srvRecs := t.expectSRVRecords(&client, domainName, len(expSRVRecs)) Expect(srvRecs).To(Equal(expSRVRecs), reportNonConformant( fmt.Sprintf("Received SRV records %v do not match the expected records %v", srvRecs, expSRVRecs))) } }) Specify("DNS lookups of the ..svc.cluster.local domain for a ClusterIP service should only resolve "+ "local services", func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") By(fmt.Sprintf("Retrieving local Service on cluster %q", clients[0].name)) var resolvedIP string Eventually(func(ctx context.Context) string { svc, err := clients[0].k8s.CoreV1().Services(t.namespace).Get(ctx, t.helloService.Name, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred(), "Error retrieving the local Service") resolvedIP = svc.Spec.ClusterIP return resolvedIP }).WithContext(ctx).Within(20*time.Second).ProbeEvery(1*time.Second).ShouldNot(BeEmpty(), "The service was not assigned a cluster IP") By(fmt.Sprintf("Found local Service cluster IP %q", resolvedIP)) // Add trailing dot to prevent search domain from being appended command := []string{"sh", "-c", fmt.Sprintf("nslookup -type=%s %s.%s.svc.cluster.local.", dnsRecordTypeOf(ipFamilyOf(resolvedIP)), t.helloService.Name, t.namespace)} By(fmt.Sprintf("Executing command %q on cluster %q", strings.Join(command, " "), clients[0].name)) t.awaitCmdOutputMatches(&clients[0], command, resolvedIP, 1, reportNonConformant("")) }) }) func (t *testDriver) expectSRVRecords(c *clusterClients, domainName string, expectedCount int) []srvRecord { // Add trailing dot to prevent search domain from being appended command := []string{"sh", "-c", "nslookup -type=SRV " + domainName + "."} By(fmt.Sprintf("Executing command %q on cluster %q", strings.Join(command, " "), c.name)) var srvRecs []srvRecord Eventually(func(g Gomega) { srvRecs = parseSRVRecords(t.execCmdOnRequestPod(c, command)) g.Expect(srvRecs).To(HaveLen(expectedCount), fmt.Sprintf("Expected %d SRV records but got %d: %v", expectedCount, len(srvRecs), srvRecs)) }, 20, 1).Should(Succeed(), reportNonConformant("")) return srvRecs } // Match SRV records from nslookup of the form: // // hello.mcs-conformance-1686874467.svc.clusterset.local service = 0 50 42 hello.mcs-conformance-1686874467.svc.clusterset.local // // to extract the port and target domain name (the last two tokens) var srvRecordRegEx = regexp.MustCompile(`.*=\s*\d*\s*\d*\s*(\d*)\s*([a-zA-Z0-9-.]*)`) type srvRecord struct { port int32 domainName string } func (s srvRecord) String() string { return fmt.Sprintf("port:%d, domainName:%q", s.port, s.domainName) } func parseSRVRecords(str string) []srvRecord { var recs []srvRecord matches := srvRecordRegEx.FindAllStringSubmatch(str, -1) for i := range matches { // First match at index 0 is the full text that was matched; index 1 is the port and index 2 is the domain name. port, _ := strconv.ParseInt(matches[i][1], 10, 32) domainName := matches[i][2] // Strip trailing period from FQDN (some nslookup versions include it) domainName = strings.TrimSuffix(domainName, ".") recs = append(recs, srvRecord{ port: int32(port), domainName: domainName, }) } return recs } ================================================ FILE: conformance/conformance_suite.go ================================================ /* 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 conformance import ( "cmp" "context" "errors" "flag" "fmt" "math/rand" "slices" "strings" "testing" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/types" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" rest "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" k8snet "k8s.io/utils/net" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" mcsclient "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" ) type clusterClients struct { name string k8s kubernetes.Interface mcs mcsclient.Interface rest *rest.Config } var ( contexts string clients []clusterClients loadingRules *clientcmd.ClientConfigLoadingRules skipVerifyEndpointSliceManagedBy bool dnsDomain string organization string project string version string url string ) // TestConformance runs the conformance test. func TestConformance(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Conformance Suite") } func init() { loadingRules = clientcmd.NewDefaultClientConfigLoadingRules() loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig flag.StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "absolute path(s) to the kubeconfig file(s)") flag.StringVar(&contexts, "contexts", "", "comma-separated list of contexts to use") flag.BoolVar(&skipVerifyEndpointSliceManagedBy, "skip-verify-eps-managed-by", false, fmt.Sprintf("The MSC spec states that any EndpointSlice created by an mcs-controller must be marked as managed by "+ "the mcs-controller. By default, the conformance test verifies that the %q label on MCS EndpointSlices is not equal to %q. "+ "However with some implementations, MCS EndpointSlices may be created and managed by K8s. If this flag is set to true, "+ "the test only verifies the presence of the label.", discoveryv1.LabelManagedBy, K8sEndpointSliceManagedByName)) flag.StringVar(&dnsDomain, "dns-domain", "clusterset.local", "The DNS domain suffix used for multi-cluster services. "+ "The default is \"clusterset.local\" as specified by the MCS spec, but some implementations may use a custom domain.") flag.StringVar(&organization, "organization", "", "Name of the organization responsible for the MCS implementation") flag.StringVar(&project, "project", "", "Name of the MCS implementation project being tested") flag.StringVar(&version, "version", "", "Version of the MCS implementation being tested") flag.StringVar(&url, "url", "", "A URL pointing to the MCS implementation project or documentation") } var _ = BeforeSuite(func(ctx context.Context) { Expect(setupClients(ctx)).To(Succeed(), "Test suite set up failed") }) func setupClients(ctx context.Context) error { splitContexts := strings.Split(contexts, ",") clients = make([]clusterClients, len(splitContexts)) accumulatedErrors := []error{} for i, kubeContext := range splitContexts { err := func() error { overrides := clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults} overrides.CurrentContext = kubeContext clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &overrides) rawConfig, err := clientConfig.RawConfig() if err != nil { return fmt.Errorf("error setting up a Kubernetes API client on context %s: %w", kubeContext, err) } name := kubeContext if name == "" { name = rawConfig.CurrentContext } configContext, ok := rawConfig.Contexts[name] if ok { name = configContext.Cluster } restConfig, err := clientConfig.ClientConfig() if err != nil { return fmt.Errorf("error setting up a Kubernetes API client on context %s: %w", name, err) } k8sClient, err := kubernetes.NewForConfig(restConfig) if err != nil { return fmt.Errorf("error setting up a Kubernetes API client on context %s: %w", name, err) } mcsClient, err := mcsclient.NewForConfig(restConfig) if err != nil { return fmt.Errorf("error setting up an MCS API client on context %s: %w", name, err) } if _, err := mcsClient.MulticlusterV1beta1().ServiceExports("").List(ctx, metav1.ListOptions{}); err != nil { return fmt.Errorf("error listing ServiceExports on context %s: %w. Is the MCS API installed?", name, err) } if _, err := mcsClient.MulticlusterV1beta1().ServiceImports("").List(ctx, metav1.ListOptions{}); err != nil { return fmt.Errorf("error listing ServiceImports on context %s: %w. Is the MCS API installed?", name, err) } clients[i] = clusterClients{name: name, k8s: k8sClient, mcs: mcsClient, rest: restConfig} return nil }() accumulatedErrors = append(accumulatedErrors, err) } return errors.Join(accumulatedErrors...) } type testDriver struct { namespace string helloService *corev1.Service helloServiceExport *v1beta1.ServiceExport helloDeployment *appsv1.Deployment requestPod *corev1.Pod autoExportService bool } func newTestDriver() *testDriver { t := &testDriver{} BeforeEach(func() { t.namespace = fmt.Sprintf("mcs-conformance-%v", rand.Uint32()) t.helloService = newHelloService() t.helloServiceExport = newHelloServiceExport() t.helloDeployment = newHelloDeployment() t.requestPod = newRequestPod() t.autoExportService = true }) JustBeforeEach(func(ctx context.Context) { Expect(clients).ToNot(BeEmpty()) // Set up the shared namespace for _, client := range clients { _, err := client.k8s.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: t.namespace}, }, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) } // Set up the remote service (the first cluster is considered to be the remote) t.helloService = t.deployHelloService(ctx, &clients[0], t.helloService) // Start the request pod on all clusters for _, client := range clients { t.startRequestPod(ctx, client) } if t.autoExportService { t.createServiceExport(ctx, &clients[0], t.helloServiceExport) } }) AfterEach(func(ctx context.Context) { // Clean up the shared namespace for _, client := range clients { err := client.k8s.CoreV1().Namespaces().Delete(ctx, t.namespace, metav1.DeleteOptions{}) if !apierrors.IsNotFound(err) { Expect(err).ToNot(HaveOccurred()) } } }) return t } func (t *testDriver) createServiceExport(ctx context.Context, c *clusterClients, serviceExport *v1beta1.ServiceExport) { _, err := c.mcs.MulticlusterV1beta1().ServiceExports(t.namespace).Create( ctx, serviceExport, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) By(fmt.Sprintf("Service \"%s/%s\" exported on cluster %q", t.namespace, helloServiceName, c.name)) } func (t *testDriver) deleteServiceExport(ctx context.Context, c *clusterClients) { Expect(c.mcs.MulticlusterV1beta1().ServiceExports(t.namespace).Delete(ctx, helloServiceName, metav1.DeleteOptions{})).ToNot(HaveOccurred()) By(fmt.Sprintf("Service \"%s/%s\" unexported on cluster %q", t.namespace, helloServiceName, c.name)) } func (t *testDriver) deployHelloService(ctx context.Context, c *clusterClients, service *corev1.Service) *corev1.Service { if t.helloDeployment != nil { _, err := c.k8s.AppsV1().Deployments(t.namespace).Create(ctx, t.helloDeployment, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) } deployed, err := c.k8s.CoreV1().Services(t.namespace).Create(ctx, service, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) By(fmt.Sprintf("Service \"%s/%s\" with IP families %v deployed on cluster %q", deployed.Namespace, deployed.Name, deployed.Spec.IPFamilies, c.name)) return deployed } func (t *testDriver) getServiceImport(ctx context.Context, c *clusterClients, name string) *v1beta1.ServiceImport { si, err := c.mcs.MulticlusterV1beta1().ServiceImports(t.namespace).Get(ctx, name, metav1.GetOptions{}) if apierrors.IsNotFound(err) || errors.Is(err, context.DeadlineExceeded) || (err != nil && strings.Contains(err.Error(), "rate limiter")) { return nil } Expect(err).ToNot(HaveOccurred(), "Error retrieving ServiceImport") return si } func (t *testDriver) awaitServiceImport(ctx context.Context, c *clusterClients, name string, reportNonConformanceOnMissing bool, verify func(Gomega, *v1beta1.ServiceImport)) *v1beta1.ServiceImport { var serviceImport *v1beta1.ServiceImport By(fmt.Sprintf("Retrieving ServiceImport for %q on cluster %q", name, c.name)) Eventually(func(g Gomega, ctx context.Context) { si := t.getServiceImport(ctx, c, name) missingMsg := fmt.Sprintf("ServiceImport was not found on cluster %q", c.name) var missing any = missingMsg if reportNonConformanceOnMissing { missing = reportNonConformant(missingMsg) } g.Expect(si).NotTo(BeNil(), missing) serviceImport = si if verify != nil { verify(g, serviceImport) } // The final run succeeded so cancel any prior non-conformance reported. cancelNonConformanceReport() }).WithContext(ctx).Within(20 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) return serviceImport } func (t *testDriver) awaitServiceImportIPFamilies(ctx context.Context, c *clusterClients) []corev1.IPFamily { serviceImport := t.awaitServiceImport(ctx, c, t.helloService.Name, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(serviceImport.Spec.IPFamilies).NotTo(BeEmpty(), "ServiceImport on cluster %q does not contain an IP family", c.name) }) return serviceImport.Spec.IPFamilies } func (t *testDriver) awaitNoServiceImport(ctx context.Context, c *clusterClients, name, nonConformanceMsg string) { Eventually(func() bool { _, err := c.mcs.MulticlusterV1beta1().ServiceImports(t.namespace).Get(ctx, name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { return true } Expect(err).ToNot(HaveOccurred()) return false }, 20*time.Second, 100*time.Millisecond).Should(BeTrue(), reportNonConformant(nonConformanceMsg)) } func (t *testDriver) ensureServiceImport(ctx context.Context, c *clusterClients, name, nonConformanceMsg string) { Consistently(func() error { _, err := c.mcs.MulticlusterV1beta1().ServiceImports(t.namespace).Get(ctx, name, metav1.GetOptions{}) return err }, 5*time.Second, 100*time.Millisecond).ShouldNot(HaveOccurred(), reportNonConformant(nonConformanceMsg)) } func (t *testDriver) ensureNoServiceImport(ctx context.Context, c *clusterClients, name, nonConformanceMsg string) { Consistently(func() bool { _, err := c.mcs.MulticlusterV1beta1().ServiceImports(t.namespace).Get(ctx, name, metav1.GetOptions{}) return apierrors.IsNotFound(err) }, 5*time.Second, 100*time.Millisecond).Should(BeTrue(), reportNonConformant(nonConformanceMsg)) } func (t *testDriver) awaitServiceExportCondition(ctx context.Context, c *clusterClients, condType v1beta1.ServiceExportConditionType, wantStatus metav1.ConditionStatus) { Eventually(func() bool { se, err := c.mcs.MulticlusterV1beta1().ServiceExports(t.namespace).Get(ctx, helloServiceName, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) cond := meta.FindStatusCondition(se.Status.Conditions, string(condType)) return cond != nil && cond.Status == wantStatus }, 20*time.Second, 100*time.Millisecond).Should(BeTrue(), reportNonConformant(fmt.Sprintf("The %s condition was not set to %s", condType, wantStatus))) } func (t *testDriver) startRequestPod(ctx context.Context, client clusterClients) { _, err := client.k8s.CoreV1().Pods(t.namespace).Create(ctx, t.requestPod, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) Eventually(func() error { pod, err := client.k8s.CoreV1().Pods(t.namespace).Get(ctx, t.requestPod.Name, metav1.GetOptions{}) if err != nil { return err } if pod.Status.Phase != corev1.PodRunning { return fmt.Errorf("pod is not running yet, current status %v", pod.Status.Phase) } return nil }, 20, 1).Should(Succeed()) } func (t *testDriver) execCmdOnRequestPod(c *clusterClients, command []string) string { stdout, _, _ := execCmd(c.k8s, c.rest, t.requestPod.Name, t.namespace, command) return string(stdout) } func (t *testDriver) awaitCmdOutputMatches(c *clusterClients, command []string, expected any, nIter int, msg func() string) { var matcher types.GomegaMatcher switch v := expected.(type) { case string: matcher = ContainSubstring(v) case types.GomegaMatcher: matcher = v } Eventually(func(g Gomega) { output := t.execCmdOnRequestPod(c, command) g.Expect(output).To(matcher, "Command output") }).Within(time.Duration(20*int64(nIter))*time.Second).ProbeEvery(time.Second).MustPassRepeatedly(nIter).Should(Succeed(), msg) } func (t *testDriver) awaitServicePodIP(ctx context.Context, c *clusterClients) string { By(fmt.Sprintf("Awaiting service deployment pod IP on cluster %q", c.name)) servicePodIP := "" Eventually(func(g Gomega, ctx context.Context) { pods, err := c.k8s.CoreV1().Pods(t.namespace).List(ctx, metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(newHelloDeployment().Spec.Selector), }) g.Expect(err).NotTo(HaveOccurred()) g.Expect(pods.Items).NotTo(BeEmpty()) servicePodIP = pods.Items[0].Status.PodIP g.Expect(servicePodIP).NotTo(BeEmpty(), "Service deployment pod was not allocated an IP") }).WithContext(ctx).Within(20 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) By(fmt.Sprintf("Retrieved service deployment pod IP %q", servicePodIP)) return servicePodIP } func (t *testDriver) execPortConnectivityCommand(ctx context.Context, port int, matchStr string, nIter int) { for _, client := range clients { for _, ipFamily := range t.awaitServiceImportIPFamilies(ctx, &client) { serviceFQDN := fmt.Sprintf("%s.%s.svc.%s", t.helloService.Name, t.namespace, dnsDomain) command := []string{"sh", "-c", ncCommand(ipFamily, serviceFQDN, port)} By(fmt.Sprintf("Executing %s command %q on cluster %q", ipFamily, strings.Join(command, " "), client.name)) t.awaitCmdOutputMatches(&client, command, matchStr, nIter, reportNonConformant("")) } } } type twoClusterTestDriver struct { *testDriver helloService2 *corev1.Service helloServiceExport2 *v1beta1.ServiceExport } func newTwoClusterTestDriver(t *testDriver) *twoClusterTestDriver { tt := &twoClusterTestDriver{testDriver: t} BeforeEach(func() { requireTwoClusters() tt.helloService2 = newHelloService() tt.helloServiceExport2 = newHelloServiceExport() t.autoExportService = false }) JustBeforeEach(func(ctx context.Context) { t.createServiceExport(ctx, &clients[0], t.helloServiceExport) // The conflict resolution policy in the MCS spec (KEP 1645) allows an implementation to favor maintaining // service continuity and avoiding potentially disruptive changes, as such, an implementation may choose the // first observed exported service when resolving conflicts. To support this, verify the ServiceImport is // created on the first cluster prior to deploying on the second cluster. t.awaitServiceImport(ctx, &clients[0], helloServiceName, false, nil) // Delay a little before deploying on the second cluster to ensure the first cluster's ServiceExport timestamp // is older so conflict checking is deterministic for implementations that use the timestamp when resolving conflicts. // Make the delay at least 1 sec as creation timestamps have seconds granularity. time.Sleep(1100 * time.Millisecond) t.deployHelloService(ctx, &clients[1], tt.helloService2) t.createServiceExport(ctx, &clients[1], tt.helloServiceExport2) }) return tt } func toMCSPorts(from []corev1.ServicePort) []v1beta1.ServicePort { var mcsPorts []v1beta1.ServicePort for _, port := range from { mcsPorts = append(mcsPorts, v1beta1.ServicePort{ Name: port.Name, Protocol: port.Protocol, Port: port.Port, AppProtocol: port.AppProtocol, }) } return sortMCSPorts(mcsPorts) } func sortMCSPorts(p []v1beta1.ServicePort) []v1beta1.ServicePort { slices.SortFunc(p, func(a, b v1beta1.ServicePort) int { return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) }) return p } func requireTwoClusters() { if len(clients) < 2 { Skip("This test requires at least 2 clusters - skipping") } } func addressTypeOf(f corev1.IPFamily) discoveryv1.AddressType { if f == corev1.IPv4Protocol { return discoveryv1.AddressTypeIPv4 } return discoveryv1.AddressTypeIPv6 } func dnsRecordTypeOf(f corev1.IPFamily) string { if f == corev1.IPv4Protocol { return "A" } return "AAAA" } func ipFamilyOf(ip string) corev1.IPFamily { f := k8snet.IPFamilyOfString(ip) Expect(f).NotTo(Equal(k8snet.IPFamilyUnknown)) if f == k8snet.IPv4 { return corev1.IPv4Protocol } return corev1.IPv6Protocol } // ncCommand creates a shell command that connects to a service using nc with the specified IP family. // The netshoot image provides nc which supports -4/-6 flags for forcing IP family. func ncCommand(ipFamily corev1.IPFamily, serviceFQDN string, port int) string { ipFlag := "-4" if ipFamily == corev1.IPv6Protocol { ipFlag = "-6" } // For IPv6, nc -6 with hostname seems to hang in IPv6-only environments // Fall back to explicit DNS resolution and pass IP directly to nc if ipFamily == corev1.IPv6Protocol { fqdnWithDot := serviceFQDN + "." return fmt.Sprintf( "addr=$(nslookup -type=AAAA %s 2>&1 | grep '^Address:' | grep -v '#' | tail -1 | awk '{print $2}'); "+ "if [ -z \"$addr\" ]; then echo 'No IPv6 address found'; exit 1; fi; "+ "echo hi | nc -v -w 2 $addr %d 2>&1", fqdnWithDot, port) } return fmt.Sprintf("echo hi | nc -v -w 2 %s %s %d 2>&1", ipFlag, serviceFQDN, port) } ================================================ FILE: conformance/conformance_suite_test.go ================================================ /* Copyright 2024 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 conformance_test import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestConformance(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Conformance Suite") } ================================================ FILE: conformance/connectivity.go ================================================ /* 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 conformance import ( "context" "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) var _ = Describe("", func() { t := newTestDriver() BeforeEach(func() { t.autoExportService = false }) Context("Connectivity to a service that is not exported", func() { It("should be inaccessible", Label(RequiredLabel), func() { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#exporting-services") By("attempting to access the remote service", func() { By("issuing a request from all clusters", func() { serviceFQDN := fmt.Sprintf("%s.%s.svc.%s", t.helloService.Name, t.namespace, dnsDomain) // Test all IP families for completeness for _, ipFamily := range t.helloService.Spec.IPFamilies { command := []string{"sh", "-c", ncCommand(ipFamily, serviceFQDN, 42)} // Run on all clusters for _, client := range clients { // Repeat multiple times for i := 0; i < 20; i++ { Expect(t.execCmdOnRequestPod(&client, command)).NotTo(ContainSubstring("pod ip"), reportNonConformant("")) } } } }) }) }) }) Context("Connectivity to an exported ClusterIP service", func() { It("should be accessible through DNS", Label(OptionalLabel, ConnectivityLabel, ClusterIPLabel), func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") By("Exporting the service", func() { // On the "remote" cluster t.createServiceExport(ctx, &clients[0], newHelloServiceExport()) }) By("Issuing a request from all clusters", func() { t.execPortConnectivityCommand(ctx, 42, "pod ip", 1) }) }) }) Context("Connectivity to a ClusterIP service existing in two clusters but exported from one", func() { BeforeEach(func() { requireTwoClusters() }) JustBeforeEach(func(ctx context.Context) { t.deployHelloService(ctx, &clients[1], newHelloService()) }) It("should only access the exporting cluster", Label(OptionalLabel, ConnectivityLabel, ClusterIPLabel), func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/1645-multi-cluster-services-api/README.md#exporting-services") By(fmt.Sprintf("Exporting the service on cluster %q", clients[0].name)) t.createServiceExport(ctx, &clients[0], newHelloServiceExport()) servicePodIP := t.awaitServicePodIP(ctx, &clients[0]) t.execPortConnectivityCommand(ctx, 42, servicePodIP, 10) }) }) Context("Connectivity to exported services with same port name but different port numbers on each cluster", func() { tt := newTwoClusterTestDriver(t) BeforeEach(func() { tt.helloService2.Spec.Ports = []corev1.ServicePort{ {Name: "tcp", Port: 4242, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(42)}, } }) It("should only route traffic to the cluster that exposes the port", Label(OptionalLabel, ConnectivityLabel, ClusterIPLabel, StrictPortConflictLabel), func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#service-port") servicePodIP := t.awaitServicePodIP(ctx, &clients[0]) t.execPortConnectivityCommand(ctx, 42, servicePodIP, 10) }) }) Context("Connectivity to exported services with different port name on each cluster", func() { tt := newTwoClusterTestDriver(t) BeforeEach(func() { tt.helloService2.Spec.Ports = []corev1.ServicePort{ {Name: "tcp2", Port: 4242, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(42)}, } }) It("should route traffic to each port only to the cluster that exposes it", Label(OptionalLabel, ConnectivityLabel, ClusterIPLabel, StrictPortConflictLabel), func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#service-port") cluster1PodIP := t.awaitServicePodIP(ctx, &clients[0]) cluster2PodIP := t.awaitServicePodIP(ctx, &clients[1]) t.execPortConnectivityCommand(ctx, 42, cluster1PodIP, 10) t.execPortConnectivityCommand(ctx, 4242, cluster2PodIP, 10) }) }) }) ================================================ FILE: conformance/endpoint_slice.go ================================================ /* Copyright 2024 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 conformance import ( "context" "fmt" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" discoveryv1 "k8s.io/api/discovery/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) // K8sEndpointSliceManagedByName is the name used for endpoint slices managed by the Kubernetes controller const K8sEndpointSliceManagedByName = "endpointslice-controller.k8s.io" var _ = Describe("", Label(OptionalLabel, EndpointSliceLabel), func() { t := newTestDriver() SpecifyWithSpecRef("Exporting a service should create an MCS EndpointSlice in the service's namespace in each cluster with the "+ "required MCS labels. Unexporting should delete the EndpointSlice.", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#using-endpointslice-objects-to-track-endpoints", func(ctx context.Context) { endpointSliceNames := make([][]string, len(clients)) for i, client := range clients { for _, ipFamily := range t.awaitServiceImportIPFamilies(ctx, &client) { eps := t.awaitMCSEndpointSlice(ctx, &client, addressTypeOf(ipFamily), nil, reportNonConformant(fmt.Sprintf( "an MCS EndpointSlice was not found on cluster %q. An MCS EndpointSlice is identified by the presence "+ "of the required MCS labels (%q and %q). "+ "If the MCS implementation does not use MCS EndpointSlices, you can specify a Ginkgo label filter using "+ "the %q label where appropriate to skip this test.", client.name, v1beta1.LabelServiceName, v1beta1.LabelSourceCluster, EndpointSliceLabel))) endpointSliceNames[i] = append(endpointSliceNames[i], eps.Name) Expect(eps.Labels).To(HaveKeyWithValue(v1beta1.LabelServiceName, t.helloService.Name), reportNonConformant(fmt.Sprintf("the MCS EndpointSlice %q does not contain the %q label referencing the service name", eps.Name, v1beta1.LabelServiceName))) Expect(eps.Labels).To(HaveKey(discoveryv1.LabelManagedBy), reportNonConformant(fmt.Sprintf("the MCS EndpointSlice %q does not contain the %q label", eps.Name, discoveryv1.LabelManagedBy))) if !skipVerifyEndpointSliceManagedBy { Expect(eps.Labels[discoveryv1.LabelManagedBy]).ToNot(Equal(K8sEndpointSliceManagedByName), reportNonConformant(fmt.Sprintf("the MCS EndpointSlice's %q label must not reference %q", discoveryv1.LabelManagedBy, K8sEndpointSliceManagedByName))) } } } t.deleteServiceExport(ctx, &clients[0]) for i, client := range clients { for _, name := range endpointSliceNames[i] { Eventually(func(ctx context.Context) bool { _, err := client.k8s.DiscoveryV1().EndpointSlices(t.namespace).Get(ctx, name, metav1.GetOptions{}) return apierrors.IsNotFound(err) }).WithContext(ctx).Within(20*time.Second).ProbeEvery(100*time.Millisecond).Should(BeTrue(), reportNonConformant(fmt.Sprintf("the EndpointSlice %q was not deleted on unexport from cluster %q", name, client.name))) } } }) }) func (t *testDriver) awaitMCSEndpointSlice(ctx context.Context, c *clusterClients, addressType discoveryv1.AddressType, verify func(Gomega, *discoveryv1.EndpointSlice), desc ...any) *discoveryv1.EndpointSlice { By(fmt.Sprintf("Retrieving %s MCS EndpointSlice for the service on cluster %q", addressType, c.name)) var endpointSlice *discoveryv1.EndpointSlice hasLabel := func(eps *discoveryv1.EndpointSlice, label string) bool { _, exists := eps.Labels[label] return exists } Eventually(func(g Gomega, ctx context.Context) { list, err := c.k8s.DiscoveryV1().EndpointSlices(t.namespace).List(ctx, metav1.ListOptions{}) g.Expect(err).ToNot(HaveOccurred(), "Error retrieving EndpointSlices") endpointSlice = nil for i := range list.Items { eps := &list.Items[i] if hasLabel(eps, v1beta1.LabelServiceName) && hasLabel(eps, v1beta1.LabelSourceCluster) && eps.AddressType == addressType && len(eps.Endpoints) > 0 { endpointSlice = eps if verify != nil { verify(g, endpointSlice) } } } g.Expect(endpointSlice).ToNot(BeNil(), desc...) // The final run succeeded so cancel any prior non-conformance reported. cancelNonConformanceReport() }).WithContext(ctx).Within(20 * time.Second).ProbeEvery(100 * time.Millisecond).Should(Succeed()) return endpointSlice } ================================================ FILE: conformance/framework.go ================================================ /* 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 conformance import ( "bytes" "context" "time" v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" rest "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" ) func execCmd(k8s kubernetes.Interface, config *rest.Config, podName string, podNamespace string, command []string) ([]byte, []byte, error) { req := k8s.CoreV1().RESTClient().Post().Resource("pods").Name(podName).Namespace(podNamespace).SubResource("exec") req.VersionedParams(&v1.PodExecOptions{ Command: command, Stdin: false, Stdout: true, Stderr: true, TTY: true, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) if err != nil { return []byte{}, []byte{}, err } var stdout, stderr bytes.Buffer ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ Stdin: nil, Stdout: &stdout, Stderr: &stderr, }) if err != nil { return []byte{}, []byte{}, err } return stdout.Bytes(), stderr.Bytes(), nil } ================================================ FILE: conformance/go.mod ================================================ module sigs.k8s.io/mcs-api/conformance go 1.23.0 require ( github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.35.1 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.32.5 k8s.io/apimachinery v0.32.5 k8s.io/client-go v0.32.5 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/mcs-api v0.3.0 ) replace sigs.k8s.io/mcs-api => .. require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) ================================================ FILE: conformance/go.sum ================================================ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk= k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64= k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM= k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0= k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= ================================================ FILE: conformance/headless_service_dns.go ================================================ /* Copyright 2025 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 conformance import ( "context" "fmt" "regexp" "slices" "strings" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/format" "github.com/onsi/gomega/types" corev1 "k8s.io/api/core/v1" discovery "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/utils/ptr" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var _ = Describe("", Label(OptionalLabel, DNSLabel, HeadlessLabel), func() { const replicas = 2 t := newTestDriver() BeforeEach(func() { t.helloService.Spec.ClusterIP = corev1.ClusterIPNone t.helloDeployment.Spec.Replicas = ptr.To(int32(replicas)) }) Specify("A DNS query of the ..svc."+dnsDomain+" domain for a headless service should return the "+ "ready endpoint addresses of all the backing pods", func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") for _, client := range clients { for _, ipFamily := range t.awaitServiceImportIPFamilies(ctx, &client) { command := []string{"sh", "-c", fmt.Sprintf("nslookup -type=%s %s.%s.svc.%s.", dnsRecordTypeOf(ipFamily), t.helloService.Name, t.namespace, dnsDomain)} endpoints := t.awaitK8sEndpoints(ctx, &clients[0], addressTypeOf(ipFamily)) var addresses []string for _, ep := range endpoints { addresses = append(addresses, ep.address) } By(fmt.Sprintf("Executing %s command %q on cluster %q", ipFamily, strings.Join(command, " "), client.name)) t.awaitCmdOutputMatches(&client, command, HaveAddresses(addresses), 1, reportNonConformant("")) } } }) Context("", func() { BeforeEach(func() { t.helloDeployment = nil }) JustBeforeEach(func(ctx context.Context) { _, err := clients[0].k8s.AppsV1().StatefulSets(t.namespace).Create(ctx, newStatefulSet(replicas), metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) }) Specify("A DNS query of the ....svc."+dnsDomain+" domain for a headless StatefulSet "+ "service should return the requested pod's endpoint address", Label(EndpointSliceLabel), func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") for _, client := range clients { for _, ipFamily := range t.awaitServiceImportIPFamilies(ctx, &client) { eps := t.awaitMCSEndpointSlice(ctx, &client, addressTypeOf(ipFamily), func(g Gomega, eps *discovery.EndpointSlice) { g.Expect(eps.Endpoints).To(HaveLen(replicas), "the MCS EndpointSlice %q does not contain the expected number of endpoints %d", eps.Name, replicas) for i := range eps.Endpoints { ep := eps.Endpoints[i] g.Expect(ptr.Deref(ep.Conditions.Ready, true)).To(BeTrue(), "the endpoint address %s in the MCS EndpointSlice %q is not ready", strings.Join(ep.Addresses, ","), eps.Name) g.Expect(ptr.Deref(ep.Hostname, "")).ToNot(BeEmpty(), "the hostname field for endpoint address %s in the MCS EndpointSlice %q is not set", strings.Join(ep.Addresses, ","), eps.Name) } }, "an MCS EndpointSlice was not found on cluster %q", client.name) clusterID := eps.Labels[v1beta1.LabelSourceCluster] for i := range eps.Endpoints { ep := &eps.Endpoints[i] // Add trailing dot to prevent search domain from being appended command := []string{"sh", "-c", fmt.Sprintf("nslookup -type=%s %s.%s.%s.%s.svc.%s.", dnsRecordTypeOf(ipFamily), ptr.Deref(ep.Hostname, ""), clusterID, t.helloService.Name, t.namespace, dnsDomain)} By(fmt.Sprintf("Executing command %q on cluster %q", strings.Join(command, " "), client.name)) t.awaitCmdOutputMatches(&client, command, HaveAddresses(ep.Addresses), 1, reportNonConformant("")) } } } }) }) Specify("A DNS SRV query of the ..svc."+dnsDomain+" domain for a headless service should return valid SRV "+ "records", func(ctx context.Context) { AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") // Collect endpoints for all IP families in the service (for dual-stack support) var allEndpoints []endpointInfo for _, ipFamily := range t.helloService.Spec.IPFamilies { endpoints := t.awaitK8sEndpoints(ctx, &clients[0], addressTypeOf(ipFamily)) allEndpoints = append(allEndpoints, endpoints...) } domainName := fmt.Sprintf("%s.%s.svc.%s", t.helloService.Name, t.namespace, dnsDomain) for _, client := range clients { srvRecs := t.expectSRVRecords(&client, domainName, len(allEndpoints)) // Verify each endpoint has a corresponding SRV record for _, ep := range allEndpoints { index := slices.IndexFunc(srvRecs, func(r srvRecord) bool { return strings.HasPrefix(r.domainName, ep.hostName) }) Expect(index).To(BeNumerically(">=", 0), reportNonConformant( fmt.Sprintf("SRV record for endpoint host name %q not received. Actual records received: %v", ep.hostName, srvRecs))) Expect(srvRecs[index].port).To(Equal(t.helloService.Spec.Ports[0].Port)) } } }) }) type endpointInfo struct { address string hostName string } func (e endpointInfo) String() string { return fmt.Sprintf("address:%q, hostName:%q", e.address, e.hostName) } func (t *testDriver) awaitK8sEndpoints(ctx context.Context, c *clusterClients, addressType discovery.AddressType) []endpointInfo { By(fmt.Sprintf("Retrieving %s K8s endpoint addresses for the service on cluster %q", addressType, c.name)) var endpoints []endpointInfo Eventually(func(g Gomega, ctx context.Context) { epsList, err := c.k8s.DiscoveryV1().EndpointSlices(t.namespace).List(ctx, metav1.ListOptions{ LabelSelector: labels.SelectorFromSet(map[string]string{ discovery.LabelServiceName: t.helloService.Name, }).String(), }) g.Expect(err).ToNot(HaveOccurred()) endpoints = nil for i := range epsList.Items { eps := &epsList.Items[i] if eps.AddressType != addressType { continue } for j := range epsList.Items[i].Endpoints { ep := &epsList.Items[i].Endpoints[j] g.Expect(ptr.Deref(ep.Conditions.Ready, true)).To(BeTrue(), "the endpoint address %s in the K8s EndpointSlice %q is not ready", strings.Join(ep.Addresses, ","), eps.Name) for _, addr := range ep.Addresses { epi := endpointInfo{address: addr} switch { case ptr.Deref(ep.Hostname, "") != "": epi.hostName = *ep.Hostname case strings.Contains(addr, "."): epi.hostName = strings.ReplaceAll(addr, ".", "-") case strings.Contains(addr, ":"): epi.hostName = strings.ReplaceAll(addr, ":", "-") } endpoints = append(endpoints, epi) } } } expCount := int(ptr.Deref(t.helloDeployment.Spec.Replicas, 1)) g.Expect(endpoints).To(HaveLen(expCount), "the K8s EndpointSlice does not contain the expected number of ready endpoints %d", expCount) // The final run succeeded so cancel any prior non-conformance reported. cancelNonConformanceReport() }).WithContext(ctx).Within(20 * time.Second).ProbeEvery(100 * time.Millisecond).Should(Succeed()) By(fmt.Sprintf("Found endpoints %v", endpoints)) return endpoints } // Match DNS records of type A from nslookup output of the form: // // Server: 10.96.0.10 // Address: 10.96.0.10:53 // // Name: hello.mcs-conformance-2021198391.svc.clusterset.local // Address: 10.244.0.52 // Name: hello.mcs-conformance-2021198391.svc.clusterset.local // Address: 10.244.0.51 // // to extract the domain addresses (in this case "10.244.0.52" and "10.244.0.51") var addressesRegEx = regexp.MustCompile(`Name:.*\s*Address:\s*(.*)`) type haveAddressesMatcher struct { expected []string } func (m *haveAddressesMatcher) Match(v interface{}) (bool, error) { matches := addressesRegEx.FindAllStringSubmatch(v.(string), -1) var actual []string for i := range matches { actual = append(actual, strings.TrimSpace(matches[i][1])) } slices.Sort(actual) return slices.Equal(actual, m.expected), nil } func (m *haveAddressesMatcher) FailureMessage(actual interface{}) string { return format.Message(actual, "to have addresses", m.expected) } func (m *haveAddressesMatcher) NegatedFailureMessage(actual interface{}) string { return format.Message(actual, "to not have addresses", m.expected) } func HaveAddresses(expected []string) types.GomegaMatcher { slices.Sort(expected) return &haveAddressesMatcher{ expected: expected, } } ================================================ FILE: conformance/k8s_objects.go ================================================ /* 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 conformance import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) const helloServiceName = "hello" func newHelloService() *corev1.Service { return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: helloServiceName, Annotations: map[string]string{"dummy-svc": "dummy"}, Labels: map[string]string{"dummy-svc": "dummy"}, }, Spec: corev1.ServiceSpec{ Selector: map[string]string{ "app": helloServiceName, }, Ports: []corev1.ServicePort{ { Name: "tcp", Port: 42, Protocol: corev1.ProtocolTCP, }, { Name: "udp", Port: 42, Protocol: corev1.ProtocolUDP, }, }, SessionAffinity: corev1.ServiceAffinityClientIP, SessionAffinityConfig: &corev1.SessionAffinityConfig{ ClientIP: &corev1.ClientIPConfig{TimeoutSeconds: ptr.To(int32(10))}, }, IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack), }, } } func newHelloServiceExport() *v1beta1.ServiceExport { return &v1beta1.ServiceExport{ ObjectMeta: metav1.ObjectMeta{ Name: helloServiceName, Annotations: map[string]string{"dummy-svcexport": "dummy"}, Labels: map[string]string{"dummy-svcexport": "dummy"}, }, } } // socatListenerScript generates a shell script that detects the pod's IP family configuration // and starts the appropriate socat listener (IPv4, IPv6, or dual-stack). func socatListenerScript(protocol string) string { return `# Detect IP families available on the pod from podIPs # The downward API provides podIPs as a plain string (single IP or space/comma-separated list) has_ipv4=false has_ipv6=false # Check for IPv4 pattern (dotted decimal notation) if echo "$MY_POD_IPS" | grep -qE '([^0-9]|^)[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}([^0-9]|$)'; then has_ipv4=true; fi # Check for IPv6 pattern (hex digits with at least two colons) if echo "$MY_POD_IPS" | grep -qE '[0-9a-fA-F]*:[0-9a-fA-F]*:[0-9a-fA-F:]*'; then has_ipv6=true; fi # Debug: log what we detected echo "DEBUG: MY_POD_IPS=$MY_POD_IPS" >&2 echo "DEBUG: has_ipv4=$has_ipv4, has_ipv6=$has_ipv6" >&2 # Extract first IP address (may be space or comma separated) first_ip=$(echo "$MY_POD_IPS" | tr ',' ' ' | awk '{print $1}') echo "DEBUG: first_ip=$first_ip" >&2 # Choose socat listener based on available IP families # Export first_ip so it's available to SYSTEM subprocesses export first_ip if $has_ipv6 && $has_ipv4; then # Dual-stack: use IPv6 socket that accepts both echo "DEBUG: Starting dual-stack listener" >&2 socat -v -v ` + protocol + `6-LISTEN:42,crlf,reuseaddr,fork,ipv6only=0 SYSTEM:'echo "pod ip $first_ip"' elif $has_ipv6; then # IPv6 only echo "DEBUG: Starting IPv6-only listener" >&2 socat -v -v ` + protocol + `6-LISTEN:42,crlf,reuseaddr,fork SYSTEM:'echo "pod ip $first_ip"' else # IPv4 only echo "DEBUG: Starting IPv4-only listener" >&2 socat -v -v ` + protocol + `-LISTEN:42,crlf,reuseaddr,fork SYSTEM:'echo "pod ip $first_ip"' fi` } func podContainers() []corev1.Container { return []corev1.Container{ { Name: "hello-tcp", Image: "alpine/socat:1.7.4.4", // Detect if pod has IPv4, IPv6, or both and use appropriate socat listener // - IPv4 only: use TCP-LISTEN // - IPv6 only: use TCP6-LISTEN (no ipv6only flag needed) // - Dual-stack: use TCP6-LISTEN with ipv6only=0 to handle both Command: []string{"/bin/sh"}, Args: []string{"-c", socatListenerScript("TCP")}, Env: []corev1.EnvVar{ { Name: "MY_POD_IPS", ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ FieldPath: "status.podIPs", }, }, }, }, }, { Name: "hello-udp", Image: "alpine/socat:1.7.4.4", // Detect if pod has IPv4, IPv6, or both and use appropriate socat listener // - IPv4 only: use UDP-LISTEN // - IPv6 only: use UDP6-LISTEN (no ipv6only flag needed) // - Dual-stack: use UDP6-LISTEN with ipv6only=0 to handle both Command: []string{"/bin/sh"}, Args: []string{"-c", socatListenerScript("UDP")}, Env: []corev1.EnvVar{ { Name: "MY_POD_IPS", ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ FieldPath: "status.podIPs", }, }, }, }, }, } } func newHelloDeployment() *appsv1.Deployment { return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: helloServiceName, }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To(int32(1)), Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": helloServiceName, }, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"app": helloServiceName}, }, Spec: corev1.PodSpec{ Containers: podContainers(), }, }, }, } } func newStatefulSet(replicas int) *appsv1.StatefulSet { return &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: helloServiceName + "-ss", }, Spec: appsv1.StatefulSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": helloServiceName, }, }, ServiceName: helloServiceName, Replicas: ptr.To(int32(replicas)), Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "app": helloServiceName, }, }, Spec: corev1.PodSpec{ Containers: podContainers(), RestartPolicy: corev1.RestartPolicyAlways, }, }, }, } } func newRequestPod() *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "request", Labels: map[string]string{"app": "request"}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "request", Image: "nicolaka/netshoot:v0.15", Args: []string{"/bin/sh", "-ec", "while :; do echo '.'; sleep 5 ; done"}, }, }, }, } } ================================================ FILE: conformance/report.go ================================================ /* Copyright 2024 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 conformance import ( _ "embed" // Needed for go:embed "errors" "fmt" "html/template" "os" "regexp" "slices" "strings" "sync/atomic" . "github.com/onsi/ginkgo/v2" "github.com/onsi/ginkgo/v2/types" . "github.com/onsi/gomega" "github.com/onsi/gomega/matchers" "gopkg.in/yaml.v3" ) const ( OptionalLabel = "Optional" RequiredLabel = "Required" DNSLabel = "DNS" ConnectivityLabel = "Connectivity" ClusterIPLabel = "ClusterIP" HeadlessLabel = "Headless" ExternalNameLabel = "ExternalName" EndpointSliceLabel = "EndpointSlice" ExportedLabelsLabel = "ExportedLabels" StrictPortConflictLabel = "StrictPortConflict" SpecRefReportEntry = "spec-ref" NonConformantReportEntry = "non-conformant" ) var reportingLabels = []string{ RequiredLabel, OptionalLabel, } //go:embed report_template.gohtml var reportHTML string type testInfo struct { Desc string Ref string Labels []string Passed bool Failed bool Skipped bool Conformant bool Message string } type testGrouping struct { Name string Tests []testInfo } type implementationInfo struct { Organization string Project string Version string URL string } var ( errorRegEx *regexp.Regexp // currentSpecNonConformanceMsg holds the last non-conformance message emitted by the current spec. Using // Eventually with a function that takes a Gomega, different assertions could report non-conformance on // successive retries, so we want to just report the last one. This also allows the last message to be cleared // (via cancelNonConformanceReport()) if it eventually succeeded. currentSpecNonConformanceMsg atomic.Value // specRefRegistry maps test description substrings to the KEP spec reference specRefRegistry = map[string]string{} ) // SpecifyWithSpecRef exist to be able to register a spec to the KEP alongside // a Specify. This is important to be able to correctly attach this spec ref // even if a test is skipped func SpecifyWithSpecRef(text string, specRef string, args ...interface{}) bool { specRefRegistry[text] = specRef return Specify(text, args...) } func lookupSpecRef(fullText string) string { for desc, ref := range specRefRegistry { if strings.Contains(fullText, desc) { return ref } } return "" } func init() { dummyErr := errors.New("dummy") // Matches gomega output for an error rendered by either the HaveOccurred or Success matchers, eg // // "Error retrieving resource // Unexpected error: // <*errors.StatusError | 0x400024e820>: // "foo" not found // { // ... // } // occurred" // // In this case, we want to match and extract: "Error retrieving resource" and ""foo" not found". errorRegEx = regexp.MustCompile(fmt.Sprintf(`\s*(.*)\s*(?:%s|%s|The function passed to)\s*.*\s*(.*)`, firstLine((&matchers.HaveOccurredMatcher{}).NegatedFailureMessage(dummyErr)), firstLine((&matchers.SucceedMatcher{}).FailureMessage(dummyErr)))) } var _ = ReportBeforeEach(func(specReport SpecReport) { cancelNonConformanceReport() if ref := lookupSpecRef(specReport.FullText()); ref != "" { AddReportEntry(SpecRefReportEntry, ref) } }) var _ = ReportAfterEach(func(specReport SpecReport) { if specReport.LeafNodeType != types.NodeTypeIt || specReport.State == types.SpecStatePending || specReport.State == types.SpecStateSkipped { return } msg := currentSpecNonConformanceMsg.Swap("") if msg != "" { AddReportEntry(NonConformantReportEntry, msg, types.ReportEntryVisibilityNever) } }) var _ = ReportAfterSuite("MCS conformance report", func(report Report) { testGroupMap := map[string]*testGrouping{} suiteFailure := "" for _, specReport := range report.SpecReports { if specReport.LeafNodeType == types.NodeTypeBeforeSuite && specReport.State == types.SpecStateFailed { suiteFailure = parseFailureMessage(specReport.FailureMessage()) continue } if specReport.LeafNodeType != types.NodeTypeIt || specReport.State == types.SpecStatePending { continue } for _, label := range reportingLabels { if !slices.Contains(specReport.Labels(), label) { continue } if testGroupMap[label] == nil { testGroupMap[label] = &testGrouping{ Name: label, } } info := testInfo{ Desc: strings.TrimSpace(specReport.FullText()), Conformant: true, } for _, currLabel := range specReport.Labels() { if currLabel != label { info.Labels = append(info.Labels, currLabel) } } slices.Sort(info.Labels) for i := range specReport.ReportEntries { switch specReport.ReportEntries[i].Name { case SpecRefReportEntry: info.Ref = specReport.ReportEntries[i].GetRawValue().(string) case NonConformantReportEntry: // An assertion reporting non-conformance may have failed initially but eventually succeeded after retries // so only report non-conformance if the spec actually failed. if specReport.State != types.SpecStatePassed { info.Conformant = false info.Message = specReport.ReportEntries[i].GetRawValue().(string) } } } if specReport.State == types.SpecStateSkipped { info.Skipped = true info.Message = parseFailureMessage(specReport.FailureMessage()) } else if specReport.State != types.SpecStatePassed && info.Conformant { // If the spec failed (ie didn't pass) not due to non-conformance then we assume it encountered // an unexpected error preventing conformance from being determined, and thus we'll report the // conformance status as unknown. info.Failed = true info.Message = parseFailureMessage(specReport.FailureMessage()) } info.Passed = !info.Failed && !info.Skipped && info.Conformant if info.Message != "" { info.Message = " - " + info.Message } testGroupMap[label].Tests = append(testGroupMap[label].Tests, info) } } testGroups := []testGrouping{} for _, l := range reportingLabels { if testGroupMap[l] != nil { slices.SortFunc(testGroupMap[l].Tests, func(a, b testInfo) int { if cmp := slices.Compare(a.Labels, b.Labels); cmp != 0 { return cmp } return strings.Compare(strings.TrimSpace(a.Desc), strings.TrimSpace(b.Desc)) }) testGroups = append(testGroups, *testGroupMap[l]) } } totalTests := 0 passedTests := 0 for _, g := range testGroups { for _, t := range g.Tests { totalTests++ if t.Passed { passedTests++ } } } data := struct { Groups []testGrouping SuiteFailure string DNSDomain string Passed int Total int Implementation implementationInfo }{ Groups: testGroups, SuiteFailure: suiteFailure, DNSDomain: dnsDomain, Passed: passedTests, Total: totalTests, Implementation: implementationInfo{ Organization: organization, Project: project, Version: version, URL: url, }, } out, err := os.Create("report.html") Expect(err).To(Succeed()) tmpl, err := template.New("report").Parse(reportHTML) Expect(err).To(Succeed()) err = tmpl.Execute(out, data) Expect(err).To(Succeed()) yamlOut, err := os.Create("report.yaml") Expect(err).To(Succeed()) err = yaml.NewEncoder(yamlOut).Encode(data) Expect(err).To(Succeed()) }) func parseFailureMessage(s string) string { // First see if the message represents an error formatted by a gomega matcher - we're interested in // extracting the optional user description passed to the gomega assertion and the actual error // string as these are the useful parts conducive for formatting in the report table. matches := errorRegEx.FindStringSubmatch(s) if len(matches) > 0 { // First match at index 0 is the full text that was matched; index 1 will be the user description // and index 2 the error string. We concatenate the latter two. msg := strings.TrimSpace(matches[1]) if msg == "" { msg = strings.TrimSpace(matches[2]) } else { msg = strings.TrimSuffix(msg, ".") + ": " + strings.TrimSpace(matches[2]) } return msg } // Fallback - just take the first line in the message. return firstLine(s) } func firstLine(s string) string { first, _, _ := strings.Cut(s, "\n") return strings.TrimSpace(first) } // reportNonConformant is intended for use as an optional description in a gomega assertion. It returns // a function that is lazily evaluated by the assertion only if a failure occurs. We take advantage of // that to add a report entry indicating non-conformance. func reportNonConformant(msg string) func() string { return func() string { currentSpecNonConformanceMsg.Store(msg) return msg } } func cancelNonConformanceReport() { currentSpecNonConformanceMsg.Store("") } ================================================ FILE: conformance/report_template.gohtml ================================================ MCS API conformance report

MCS Conformance Report

{{.Passed}} of {{.Total}} tests passed

{{if and .DNSDomain (ne .DNSDomain "clusterset.local")}}

DNS domain suffix: {{.DNSDomain}}

{{end}} {{if .SuiteFailure }}

{{.SuiteFailure}}

{{end}} {{range .Groups}}

{{.Name}}

{{range .Tests}} {{ if .Skipped }} {{ else if .Failed }} {{ else if .Conformant }} {{ else }} {{end}} {{end}}
Conformant Labels Description
Skipped{{.Message}}Unknown{{.Message}}Yes{{.Message}}No{{.Message}}{{range $i, $l := .Labels}}{{if $i}}, {{end}}{{$l}}{{end}} {{.Desc}}
{{end}} ================================================ FILE: conformance/service_import.go ================================================ /* Copyright 2024 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 conformance import ( "context" "fmt" "net" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var ( _ = Describe("", testGeneralServiceImport) _ = Describe("", Label(ClusterIPLabel), testClusterIPServiceImport) _ = Describe("", Label(HeadlessLabel), testHeadlessServiceImport) _ = Describe("", Label(ExternalNameLabel), testExternalNameService) _ = Describe("", testServiceTypeConflict) ) func testGeneralServiceImport() { t := newTestDriver() assertHasKeyValues := func(g Gomega, actual, expected map[string]string) { for k, v := range expected { g.Expect(actual).To(HaveKeyWithValue(k, v), reportNonConformant("")) } } assertNotHasKeyValues := func(g Gomega, actual, expected map[string]string) { for k, v := range expected { g.Expect(actual).ToNot(HaveKeyWithValue(k, v), reportNonConformant("")) } } // Other tests also unexport the service and verifies the ServiceImport is deleted, but it doesn't require more than one // cluster, so it provides basic coverage in a simple single cluster environment. The following test is more comprehensive in that it // exports a service from two clusters and ensures proper behavior when unexported from both. SpecifyWithSpecRef("A ServiceImport should only exist as long as there's at least one exporting cluster", "https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/1645-multi-cluster-services-api/README.md#importing-services", Label(RequiredLabel), func(ctx context.Context) { requireTwoClusters() t.awaitServiceImport(ctx, &clients[0], t.helloService.Name, false, nil) By(fmt.Sprintf("Exporting the service on the second cluster %q", clients[1].name)) t.deployHelloService(ctx, &clients[1], newHelloService()) t.createServiceExport(ctx, &clients[1], newHelloServiceExport()) // Sanity check and to also wait a bit for the second cluster to export. There's no deterministic way to tell if/when // the second cluster has finished exporting other than utilizing different service ports in each cluster but service // port merging behavior is already covered in another test case, so it's ideal not to rely on behavior that could // cause orthogonal failures and to avoid duplicate testing. t.ensureServiceImport(ctx, &clients[0], t.helloService.Name, fmt.Sprintf( "the ServiceImport no longer exists after exporting on cluster %q", clients[1].name)) t.deleteServiceExport(ctx, &clients[0]) t.ensureServiceImport(ctx, &clients[0], t.helloService.Name, fmt.Sprintf( "the ServiceImport no longer exists after unexporting the service on cluster %q while still exported on cluster %q", clients[0].name, clients[1].name)) t.deleteServiceExport(ctx, &clients[1]) t.awaitNoServiceImport(ctx, &clients[0], helloServiceName, "the ServiceImport still exists after unexporting the service on all clusters") }) Context("", func() { BeforeEach(func() { t.helloServiceExport.Spec.ExportedAnnotations = map[string]string{"dummy-annotation": "true"} t.helloServiceExport.Spec.ExportedLabels = map[string]string{"dummy-label": "true"} }) SpecifyWithSpecRef("Only labels and annotations specified as exported in the ServiceExport should be propagated to the ServiceImport", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#labels-and-annotations", Label(OptionalLabel), Label(ExportedLabelsLabel), func(ctx context.Context) { t.awaitServiceImport(ctx, &clients[0], helloServiceName, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { assertHasKeyValues(g, serviceImport.Annotations, t.helloServiceExport.Spec.ExportedAnnotations) assertNotHasKeyValues(g, serviceImport.Annotations, t.helloService.Annotations) assertHasKeyValues(g, serviceImport.Labels, t.helloServiceExport.Spec.ExportedLabels) assertNotHasKeyValues(g, serviceImport.Labels, t.helloService.Labels) }) }) }) Context("A service exported on two clusters", func() { tt := newTwoClusterTestDriver(t) Context("with conflicting annotations and labels", func() { BeforeEach(func() { t.helloServiceExport.Spec.ExportedAnnotations = map[string]string{"dummy-annotation": "true"} t.helloServiceExport.Spec.ExportedLabels = map[string]string{"dummy-label": "true"} tt.helloServiceExport2.Spec.ExportedAnnotations = map[string]string{"dummy-annotation2": "true"} tt.helloServiceExport2.Spec.ExportedLabels = map[string]string{"dummy-label2": "true"} }) SpecifyWithSpecRef("should apply the conflict resolution policy and report a Conflict condition on each ServiceExport", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#labels-and-annotations", Label(OptionalLabel), Label(ExportedLabelsLabel), func(ctx context.Context) { t.awaitServiceExportCondition(ctx, &clients[0], v1beta1.ServiceExportConditionConflict, metav1.ConditionTrue) t.awaitServiceExportCondition(ctx, &clients[1], v1beta1.ServiceExportConditionConflict, metav1.ConditionTrue) t.awaitServiceImport(ctx, &clients[0], t.helloService.Name, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { assertHasKeyValues(g, serviceImport.Annotations, t.helloServiceExport.Spec.ExportedAnnotations) assertNotHasKeyValues(g, serviceImport.Annotations, tt.helloServiceExport2.Spec.ExportedAnnotations) assertHasKeyValues(g, serviceImport.Labels, t.helloServiceExport.Spec.ExportedLabels) assertNotHasKeyValues(g, serviceImport.Labels, tt.helloServiceExport2.Spec.ExportedLabels) }) }) }) }) } func testClusterIPServiceImport() { t := newTestDriver() SpecifyWithSpecRef("Exporting a ClusterIP service should create a ServiceImport of type ClusterSetIP in the service's namespace in each cluster. "+ "Unexporting should delete the ServiceImport", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#importing-services", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceExportCondition(ctx, &clients[0], v1beta1.ServiceExportConditionValid, metav1.ConditionTrue) for i := range clients { serviceImport := t.awaitServiceImport(ctx, &clients[i], helloServiceName, true, nil) Expect(serviceImport.Spec.Type).To(Equal(v1beta1.ClusterSetIP), reportNonConformant( fmt.Sprintf("ServiceImport on cluster %q has type %q", clients[i].name, serviceImport.Spec.Type))) } t.deleteServiceExport(ctx, &clients[0]) for i := range clients { t.awaitNoServiceImport(ctx, &clients[i], helloServiceName, fmt.Sprintf( "the ServiceImport still exists on cluster %q after unexporting the service", clients[i].name)) } }) SpecifyWithSpecRef("The SessionAffinity for a ClusterSetIP ServiceImport should match the exported service's SessionAffinity", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#session-affinity", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceImport(ctx, &clients[0], helloServiceName, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(serviceImport.Spec.SessionAffinity).To(Equal(t.helloService.Spec.SessionAffinity), reportNonConformant("")) g.Expect(serviceImport.Spec.SessionAffinityConfig).To(Equal(t.helloService.Spec.SessionAffinityConfig), reportNonConformant( "The SessionAffinityConfig of the ServiceImport does not match the exported Service's SessionAffinityConfig")) }) }) Context("", func() { BeforeEach(func() { t.helloService.Spec.InternalTrafficPolicy = ptr.To(corev1.ServiceInternalTrafficPolicyCluster) }) SpecifyWithSpecRef("The InternalTrafficPolicy for a ClusterSetIP ServiceImport should match the exported service's InternalTrafficPolicy", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#internal-traffic-policy", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceImport(ctx, &clients[0], helloServiceName, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(serviceImport.Spec.InternalTrafficPolicy).To(Equal(t.helloService.Spec.InternalTrafficPolicy), reportNonConformant( "The InternalTrafficPolicy of the ServiceImport does not match the exported Service's InternalTrafficPolicy")) }) }, ) }) Context("", func() { BeforeEach(func() { t.helloService.Spec.TrafficDistribution = ptr.To(corev1.ServiceTrafficDistributionPreferClose) }) SpecifyWithSpecRef("The TrafficDistribution for a ClusterSetIP ServiceImport should match the exported service's TrafficDistribution", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#traffic-distribution", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceImport(ctx, &clients[0], helloServiceName, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(serviceImport.Spec.TrafficDistribution).To(Equal(t.helloService.Spec.TrafficDistribution), reportNonConformant( "The TrafficDistribution of the ServiceImport does not match the exported Service's TrafficDistribution")) }) }, ) }) SpecifyWithSpecRef("An IP should be allocated for a ClusterSetIP ServiceImport for each IP family", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#clustersetip", Label(RequiredLabel), func(ctx context.Context) { serviceImport := t.awaitServiceImport(ctx, &clients[0], t.helloService.Name, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(serviceImport.Spec.IPs).ToNot(BeEmpty(), reportNonConformant("")) }) Expect(serviceImport.Spec.IPs).To(HaveLen(len(serviceImport.Spec.IPFamilies)), reportNonConformant( "The ServiceImport IPs field must have the same number of IPs as ServiceImport IPFamilies")) // Verify the IPs are valid and are in the same order as IPFamilies. for i := range serviceImport.Spec.IPs { Expect(net.ParseIP(serviceImport.Spec.IPs[i])).ToNot(BeNil(), reportNonConformant(fmt.Sprintf("The value %q is not a valid IP", serviceImport.Spec.IPs[i]))) Expect(ipFamilyOf(serviceImport.Spec.IPs[i])).To(Equal(serviceImport.Spec.IPFamilies[i]), reportNonConformant(fmt.Sprintf( "The IP family of ServiceImport.Spec.IPs[%d] (%q) must match ServiceImport.Spec.IPFamilies[%d] (%q)", i, serviceImport.Spec.IPs[i], i, serviceImport.Spec.IPFamilies[i]))) } }) SpecifyWithSpecRef("The ports for a ClusterSetIP ServiceImport should match those of the exported service", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#service-port", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceImport(ctx, &clients[0], helloServiceName, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(sortMCSPorts(serviceImport.Spec.Ports)).To(Equal(toMCSPorts(t.helloService.Spec.Ports)), reportNonConformant("")) }) }) Context("A ClusterIP service exported on two clusters", func() { tt := newTwoClusterTestDriver(t) Context("", func() { BeforeEach(func() { tt.helloService2.Spec.Ports = []corev1.ServicePort{ t.helloService.Spec.Ports[0], { Name: "stcp", Port: 142, Protocol: corev1.ProtocolSCTP, }, } }) SpecifyWithSpecRef("should expose the union of the constituent service ports and raise a conflict", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#service-port", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceExportCondition(ctx, &clients[0], v1beta1.ServiceExportConditionConflict, metav1.ConditionTrue) t.awaitServiceExportCondition(ctx, &clients[1], v1beta1.ServiceExportConditionConflict, metav1.ConditionTrue) t.awaitServiceImport(ctx, &clients[0], t.helloService.Name, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(sortMCSPorts(serviceImport.Spec.Ports)).To(Equal(toMCSPorts( append(t.helloService.Spec.Ports, tt.helloService2.Spec.Ports[1]))), reportNonConformant("")) }) }) }) Context("with conflicting ports", Label(RequiredLabel), func() { BeforeEach(func() { tt.helloService2.Spec.Ports = []corev1.ServicePort{t.helloService.Spec.Ports[0]} tt.helloService2.Spec.Ports[0].Port = t.helloService.Spec.Ports[0].Port + 1 }) SpecifyWithSpecRef("should apply the conflict resolution policy and report a Conflict condition on each ServiceExport", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#service-port", func(ctx context.Context) { t.awaitServiceExportCondition(ctx, &clients[0], v1beta1.ServiceExportConditionConflict, metav1.ConditionTrue) t.awaitServiceExportCondition(ctx, &clients[1], v1beta1.ServiceExportConditionConflict, metav1.ConditionTrue) t.awaitServiceImport(ctx, &clients[0], t.helloService.Name, false, func(g Gomega, serviceImport *v1beta1.ServiceImport) { g.Expect(sortMCSPorts(serviceImport.Spec.Ports)).To(Equal(toMCSPorts(t.helloService.Spec.Ports)), reportNonConformant("The service ports were not resolved correctly")) }) }) }) }) } func testHeadlessServiceImport() { t := newTestDriver() BeforeEach(func() { t.helloService.Spec.ClusterIP = corev1.ClusterIPNone }) SpecifyWithSpecRef("Exporting a headless service should create a ServiceImport of type Headless in the service's namespace in each cluster. "+ "Unexporting should delete the ServiceImport", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#service-types", Label(RequiredLabel), func(ctx context.Context) { for i := range clients { serviceImport := t.awaitServiceImport(ctx, &clients[i], helloServiceName, true, nil) Expect(serviceImport.Spec.Type).To(Equal(v1beta1.Headless), reportNonConformant( fmt.Sprintf("ServiceImport on cluster %q has type %q", clients[i].name, serviceImport.Spec.Type))) } t.deleteServiceExport(ctx, &clients[0]) for i := range clients { t.awaitNoServiceImport(ctx, &clients[i], helloServiceName, fmt.Sprintf( "the ServiceImport still exists on cluster %q after unexporting the service", clients[i].name)) } }) SpecifyWithSpecRef("No clusterset IP should be allocated for a Headless ServiceImport", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#clustersetip", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceImport(ctx, &clients[0], t.helloService.Name, false, nil) Consistently(func() []string { return t.getServiceImport(ctx, &clients[0], t.helloService.Name).Spec.IPs }).Within(5*time.Second).ProbeEvery(time.Second).Should(BeEmpty(), reportNonConformant("")) }) } func testExternalNameService() { t := newTestDriver() BeforeEach(func() { t.helloService.Spec.Type = corev1.ServiceTypeExternalName t.helloService.Spec.ExternalName = "example.com" t.helloService.Spec.IPFamilyPolicy = nil }) SpecifyWithSpecRef("Exporting an ExternalName service should set ServiceExport Valid condition to False", "https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/1645-multi-cluster-services-api/README.md#service-types", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceExportCondition(ctx, &clients[0], v1beta1.ServiceExportConditionValid, metav1.ConditionFalse) t.ensureNoServiceImport(ctx, &clients[0], helloServiceName, "the ServiceImport should not exist for an ExternalName service") }) } func testServiceTypeConflict() { t := newTwoClusterTestDriver(newTestDriver()) BeforeEach(func() { t.helloService2.Spec.ClusterIP = corev1.ClusterIPNone }) SpecifyWithSpecRef("A service exported on two clusters with conflicting headlessness should apply the conflict resolution policy and "+ "report a Conflict condition on the ServiceExport", "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#headlessness", Label(RequiredLabel), func(ctx context.Context) { t.awaitServiceExportCondition(ctx, &clients[0], v1beta1.ServiceExportConditionConflict, metav1.ConditionTrue) t.awaitServiceExportCondition(ctx, &clients[1], v1beta1.ServiceExportConditionConflict, metav1.ConditionTrue) for i := range clients { serviceImport := t.awaitServiceImport(ctx, &clients[i], helloServiceName, true, nil) Expect(serviceImport.Spec.Type).To(Equal(v1beta1.ClusterSetIP), reportNonConformant( fmt.Sprintf("ServiceImport on cluster %q has type %q", clients[i].name, serviceImport.Spec.Type))) } }) } ================================================ FILE: controllers/cmd/servicecontroller/servicecontroller.go ================================================ /* Copyright 2020 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 main import ( "flag" "os" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/mcs-api/controllers" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { clientgoscheme.AddToScheme(scheme) v1beta1.AddToScheme(scheme) } func main() { var metricsAddr string var enableLeaderElection bool flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") flag.Parse() opts := ctrl.Options{ Scheme: scheme, Metrics: server.Options{ BindAddress: metricsAddr, }, LeaderElection: enableLeaderElection, WebhookServer: webhook.NewServer(webhook.Options{ Port: 9443, }), } ctrl.SetLogger(zap.New(zap.UseDevMode(true))) if err := controllers.Start(ctrl.SetupSignalHandler(), ctrl.GetConfigOrDie(), setupLog, opts); err != nil { setupLog.Error(err, "problem running controllers") os.Exit(1) } } ================================================ FILE: controllers/common.go ================================================ /* Copyright 2020 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 controllers import ( "context" "crypto/sha256" "encoding/base32" "os" "strings" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" ) const ( // DerivedServiceAnnotation is set on a ServiceImport to reference the // derived Service that represents the imported service for kube-proxy. DerivedServiceAnnotation = "multicluster.kubernetes.io/derived-service" serviceImportKind = "ServiceImport" ) func derivedName(name types.NamespacedName) string { hash := sha256.New() hash.Write([]byte(name.String())) return "derived-" + strings.ToLower(base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(hash.Sum(nil)))[:10] } // Start the controllers with the supplied config func Start(ctx context.Context, cfg *rest.Config, setupLog logr.Logger, opts ctrl.Options) error { mgr, err := ctrl.NewManager(cfg, opts) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } if err = (&ServiceImportReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("ServiceImport"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ServiceImport") return err } if err = (&ServiceReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("Service"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Service") return err } if err = (&EndpointSliceReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("EndpointSlice"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "EndpointSlice") return err } setupLog.Info("starting manager") if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") return err } return nil } ================================================ FILE: controllers/controllers_suite_test.go ================================================ /* Copyright 2020 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 controllers import ( "context" "fmt" "math/rand" "path/filepath" "testing" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/kind/pkg/cluster" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) const ( clusterName = "test-cluster" ) var ( cfg *rest.Config k8s client.Client env *envtest.Environment clusterProvider *cluster.Provider testNS string ) var _ = BeforeSuite(func(done Done) { rand.Seed(GinkgoRandomSeed()) log.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter))) // Use Kind for a more up-to-date K8s clusterProvider = cluster.NewProvider() Expect(clusterProvider.Create(clusterName)).To(Succeed()) kubeconfig, err := clusterProvider.KubeConfig(clusterName, false) Expect(err).ToNot(HaveOccurred()) cfg, err := clientcmd.RESTConfigFromKubeConfig([]byte(kubeconfig)) Expect(err).ToNot(HaveOccurred()) scheme := runtime.NewScheme() Expect(clientgoscheme.AddToScheme(scheme)).To(Succeed()) Expect(v1beta1.AddToScheme(scheme)).To(Succeed()) Expect(err).ToNot(HaveOccurred()) existingCluster := true env = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd")}, UseExistingCluster: &existingCluster, Config: cfg, } cfg, err = env.Start() Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) k8s, err = client.New(cfg, client.Options{Scheme: scheme}) Expect(err).ToNot(HaveOccurred()) Expect(k8s).ToNot(BeNil()) testNS = fmt.Sprintf("test-%v", time.Now().Unix()) Expect(k8s.Create(context.Background(), &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: testNS, }, })).To(Succeed()) opts := ctrl.Options{ Scheme: scheme, } go Start(context.TODO(), cfg, log.Log, opts) close(done) }) var _ = AfterSuite(func() { Expect(clusterProvider.Delete(clusterName, "")).To(Succeed()) err := env.Stop() Expect(err).ToNot(HaveOccurred()) }) func TestControllers(t *testing.T) { RegisterFailHandler(Fail) } ================================================ FILE: controllers/endpointslice.go ================================================ /* Copyright 2020 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 controllers import ( "context" "github.com/go-logr/logr" discoveryv1 "k8s.io/api/discovery/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) // EndpointSliceReconciler reconciles a EndpointSlice object type EndpointSliceReconciler struct { client.Client Log logr.Logger } // +kubebuilder:rbac:groups=discovery.k8s.io,resources=endpointslices,verbs=get;list;watch;update;patch func shouldIgnoreEndpointSlice(epSlice *discoveryv1.EndpointSlice) bool { if epSlice.DeletionTimestamp != nil { return true } if epSlice.Labels[v1beta1.LabelServiceName] == "" { return true } return false } // Reconcile the changes. func (r *EndpointSliceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("endpointslice", req.NamespacedName) var epSlice discoveryv1.EndpointSlice if err := r.Client.Get(ctx, req.NamespacedName, &epSlice); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if shouldIgnoreEndpointSlice(&epSlice) { return ctrl.Result{}, nil } // Ensure the EndpointSlice is labelled to match the ServiceImport's derived // Service. serviceName := derivedName(types.NamespacedName{Namespace: epSlice.Namespace, Name: epSlice.Labels[v1beta1.LabelServiceName]}) if epSlice.Labels[discoveryv1.LabelServiceName] == serviceName { return ctrl.Result{}, nil } epSlice.Labels[discoveryv1.LabelServiceName] = serviceName if err := r.Client.Update(ctx, &epSlice); err != nil { return ctrl.Result{}, err } log.Info("added label", discoveryv1.LabelServiceName, serviceName) return ctrl.Result{}, nil } // SetupWithManager wires up the controller. func (r *EndpointSliceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr).For(&discoveryv1.EndpointSlice{}).Complete(r) } ================================================ FILE: controllers/endpointslice_test.go ================================================ /* Copyright 2020 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 controllers import ( "context" "fmt" "math/rand" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var _ = Describe("EndpointSlice", func() { ctx := context.Background() Context("should be ignored", func() { Specify("when not multi-cluster", func() { Expect(shouldIgnoreEndpointSlice(&discoveryv1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: "no-mc-service", }, AddressType: discoveryv1.AddressTypeIPv4, })).To(BeTrue()) }) Specify("when deleted", func() { Expect(shouldIgnoreEndpointSlice(&discoveryv1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: "deleted", DeletionTimestamp: &metav1.Time{Time: time.Now()}, }, AddressType: discoveryv1.AddressTypeIPv4, })).To(BeTrue()) }) }) Context("created with mc label", func() { var ( serviceName types.NamespacedName derivedServiceName types.NamespacedName sliceName types.NamespacedName epSlice discoveryv1.EndpointSlice ) BeforeEach(func() { serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} sliceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("slice-%v", rand.Uint64())} epSlice = discoveryv1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: sliceName.Name, Labels: map[string]string{ v1beta1.LabelServiceName: serviceName.Name, }, }, AddressType: discoveryv1.AddressTypeIPv4, } Expect(k8s.Create(ctx, &epSlice)).To(Succeed()) }) It("has correct label", func() { Eventually(func() string { var eps discoveryv1.EndpointSlice Expect(k8s.Get(ctx, sliceName, &eps)).Should(Succeed()) return eps.Labels[discoveryv1.LabelServiceName] }).Should(Equal(derivedServiceName.Name)) }) }) Context("created with wrong label", func() { var ( serviceName types.NamespacedName derivedServiceName types.NamespacedName sliceName types.NamespacedName epSlice discoveryv1.EndpointSlice ) BeforeEach(func() { serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} sliceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("slice-%v", rand.Uint64())} epSlice = discoveryv1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: sliceName.Name, Labels: map[string]string{ v1beta1.LabelServiceName: serviceName.Name, discoveryv1.LabelServiceName: serviceName.Name, }, }, AddressType: discoveryv1.AddressTypeIPv4, } Expect(k8s.Create(ctx, &epSlice)).To(Succeed()) }) It("has correct label", func() { Eventually(func() string { var eps discoveryv1.EndpointSlice Expect(k8s.Get(ctx, sliceName, &eps)).Should(Succeed()) return eps.Labels[discoveryv1.LabelServiceName] }).Should(Equal(derivedServiceName.Name)) }) }) }) ================================================ FILE: controllers/go.mod ================================================ module sigs.k8s.io/mcs-api/controllers go 1.23.0 require ( github.com/go-logr/logr v1.4.2 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 k8s.io/api v0.32.5 k8s.io/apimachinery v0.32.5 k8s.io/client-go v0.32.5 sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/kind v0.23.0 sigs.k8s.io/mcs-api v0.3.0 ) replace sigs.k8s.io/mcs-api => .. require ( github.com/BurntSushi/toml v1.0.0 // indirect github.com/alessio/shellescape v1.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.32.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) ================================================ FILE: controllers/go.sum ================================================ github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk= k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64= k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM= k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0= k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/kind v0.23.0 h1:8fyDGWbWTeCcCTwA04v4Nfr45KKxbSPH1WO9K+jVrBg= sigs.k8s.io/kind v0.23.0/go.mod h1:ZQ1iZuJLh3T+O8fzhdi3VWcFTzsdXtNv2ppsHc8JQ7s= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= ================================================ FILE: controllers/service.go ================================================ /* Copyright 2020 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 controllers import ( "context" "slices" "github.com/go-logr/logr" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) // ServiceReconciler reconciles a Service object type ServiceReconciler struct { client.Client Log logr.Logger } // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch func serviceImportOwner(refs []metav1.OwnerReference) string { for _, ref := range refs { if ref.APIVersion == v1beta1.GroupVersion.String() && ref.Kind == serviceImportKind { return ref.Name } } return "" } // Reconcile the changes. func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("service", req.NamespacedName) var service v1.Service if err := r.Client.Get(ctx, req.NamespacedName, &service); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if service.DeletionTimestamp != nil { return ctrl.Result{}, nil } importName := serviceImportOwner(service.OwnerReferences) if importName == "" { return ctrl.Result{}, nil } var svcImport v1beta1.ServiceImport if err := r.Client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: importName}, &svcImport); err != nil { return ctrl.Result{}, err } ipsLen := min(2, len(service.Spec.ClusterIPs)) desiredIPs := service.Spec.ClusterIPs[:ipsLen] if service.Spec.ClusterIP == v1.ClusterIPNone { desiredIPs = []string{} } if slices.Equal(desiredIPs, svcImport.Spec.IPs) { return ctrl.Result{}, nil } svcImport.Spec.IPs = desiredIPs if err := r.Client.Update(ctx, &svcImport); err != nil { return ctrl.Result{}, err } log.Info("updated serviceimport ip", "ip", service.Spec.ClusterIP) return ctrl.Result{}, nil } // SetupWithManager wires up the controller. func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr).For(&v1.Service{}).Complete(r) } ================================================ FILE: controllers/serviceimport.go ================================================ /* Copyright 2020 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 controllers import ( "context" "github.com/go-logr/logr" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) // ServiceImportReconciler reconciles a ServiceImport object type ServiceImportReconciler struct { client.Client Log logr.Logger } // +kubebuilder:rbac:groups=multicluster.x-k8s.io,resources=serviceimports,verbs=get;list;watch;update;patch func servicePorts(svcImport *v1beta1.ServiceImport) []v1.ServicePort { ports := make([]v1.ServicePort, len(svcImport.Spec.Ports)) for i, p := range svcImport.Spec.Ports { ports[i] = v1.ServicePort{ Name: p.Name, Protocol: p.Protocol, Port: p.Port, AppProtocol: p.AppProtocol, } } return ports } func shouldIgnoreImport(svcImport *v1beta1.ServiceImport) bool { if svcImport.DeletionTimestamp != nil { return true } if svcImport.Spec.Type != v1beta1.ClusterSetIP { return true } return false } // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch // +kubebuilder:rbac:groups=core,resources=services/status,verbs=get;list;watch;create;update;patch // Reconcile the changes. func (r *ServiceImportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { serviceName := derivedName(req.NamespacedName) log := r.Log.WithValues("serviceimport", req.NamespacedName, "derived", serviceName) var svcImport v1beta1.ServiceImport if err := r.Client.Get(ctx, req.NamespacedName, &svcImport); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } if shouldIgnoreImport(&svcImport) { return ctrl.Result{}, nil } // Ensure the existence of the derived service var svc v1.Service if svcImport.Annotations[DerivedServiceAnnotation] == "" { if svcImport.Annotations == nil { svcImport.Annotations = map[string]string{} } svcImport.Annotations[DerivedServiceAnnotation] = derivedName(req.NamespacedName) if err := r.Client.Update(ctx, &svcImport); err != nil { return ctrl.Result{}, err } log.Info("added annotation", DerivedServiceAnnotation, svcImport.Annotations[DerivedServiceAnnotation]) return ctrl.Result{}, nil } if err := r.Client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: svcImport.Annotations[DerivedServiceAnnotation]}, &svc); err == nil { return ctrl.Result{}, nil } else if !apierrors.IsNotFound(err) { return ctrl.Result{}, err } svc = v1.Service{ ObjectMeta: metav1.ObjectMeta{ Namespace: req.Namespace, Name: svcImport.Annotations[DerivedServiceAnnotation], OwnerReferences: []metav1.OwnerReference{ { Name: req.Name, Kind: serviceImportKind, APIVersion: v1beta1.GroupVersion.String(), UID: svcImport.UID, }, }, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeClusterIP, Ports: servicePorts(&svcImport), }, } if err := r.Client.Create(ctx, &svc); err != nil { return ctrl.Result{}, err } log.Info("created service") if len(svcImport.Spec.IPs) == 0 { return ctrl.Result{}, nil } // update loadbalanacer status with provided clustersetIPs ingress := []v1.LoadBalancerIngress{} for _, ip := range svcImport.Spec.IPs { ingress = append(ingress, v1.LoadBalancerIngress{ IP: ip, }) } svc.Status = v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: ingress, }, } if err := r.Client.Status().Update(ctx, &svc); err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil } // SetupWithManager wires up the controller. func (r *ServiceImportReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr).For(&v1beta1.ServiceImport{}).Complete(r) } ================================================ FILE: controllers/serviceimport_test.go ================================================ /* Copyright 2020 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 controllers import ( "context" "fmt" "math/rand" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var _ = Describe("ServiceImport", func() { var ( serviceImport v1beta1.ServiceImport serviceName types.NamespacedName derivedServiceName types.NamespacedName ) ctx := context.Background() Context("should be ignored", func() { Specify("when headless", func() { Expect(shouldIgnoreImport(&v1beta1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: "headless", }, Spec: v1beta1.ServiceImportSpec{ Type: v1beta1.Headless, Ports: []v1beta1.ServicePort{ {Port: 80}, }, }, })).To(BeTrue()) }) Specify("when deleted", func() { Expect(shouldIgnoreImport(&v1beta1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: "deleted", DeletionTimestamp: &metav1.Time{Time: time.Now()}, }, Spec: v1beta1.ServiceImportSpec{ Type: v1beta1.ClusterSetIP, Ports: []v1beta1.ServicePort{ {Port: 80}, }, }, })).To(BeTrue()) }) }) Context("created", func() { BeforeEach(func() { serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} serviceImport = v1beta1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: serviceName.Name, }, Spec: v1beta1.ServiceImportSpec{ Type: v1beta1.ClusterSetIP, Ports: []v1beta1.ServicePort{ {Port: 80}, }, }, } Expect(k8s.Create(ctx, &serviceImport)).To(Succeed()) }) It("has derived service annotation", func() { Eventually(func() string { var s v1beta1.ServiceImport Expect(k8s.Get(ctx, serviceName, &s)).To(Succeed()) return s.Annotations[DerivedServiceAnnotation] }, 10).Should(Equal(derivedName(serviceName))) }, 10) It("has derived service IP", func() { var s v1beta1.ServiceImport Eventually(func() string { Expect(k8s.Get(ctx, serviceName, &s)).To(Succeed()) if len(s.Spec.IPs) > 0 { return s.Spec.IPs[0] } return "" }, 10).ShouldNot(BeEmpty()) }, 15) It("created derived service", func() { var s v1.Service Eventually(func() error { return k8s.Get(ctx, derivedServiceName, &s) }, 10).Should(Succeed()) Expect(len(s.OwnerReferences)).To(Equal(1)) Expect(s.OwnerReferences[0].UID).To(Equal(serviceImport.UID)) }, 15) It("removes derived service", func() { var s v1.Service Eventually(func() error { return k8s.Get(ctx, derivedServiceName, &s) }, 10).Should(Succeed()) var imp v1beta1.ServiceImport Expect(k8s.Get(ctx, serviceName, &imp)).To(Succeed()) Expect(k8s.Delete(ctx, &imp)).To(Succeed()) Eventually(func() error { return k8s.Get(ctx, derivedServiceName, &s) }, 15).ShouldNot(Succeed()) }, 15) }) Context("created with IP", func() { BeforeEach(func() { serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} serviceImport = v1beta1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: serviceName.Name, }, Spec: v1beta1.ServiceImportSpec{ Type: v1beta1.ClusterSetIP, Ports: []v1beta1.ServicePort{ {Port: 80}, }, }, } Expect(k8s.Create(ctx, &serviceImport)).To(Succeed()) }) It("updates derived service IP", func() { var svcImport v1beta1.ServiceImport var s v1.Service Eventually(func() error { return k8s.Get(ctx, derivedServiceName, &s) }, 10).Should(Succeed()) Eventually(func() string { Expect(k8s.Get(ctx, serviceName, &svcImport)).To(Succeed()) if len(svcImport.Spec.IPs) > 0 { return svcImport.Spec.IPs[0] } return "" }, 10).Should(Equal(s.Spec.ClusterIP)) }, 15) }) Context("created with existing clustersetIP", func() { BeforeEach(func() { serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} serviceImport = v1beta1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNS, Name: serviceName.Name, }, Spec: v1beta1.ServiceImportSpec{ Type: v1beta1.ClusterSetIP, Ports: []v1beta1.ServicePort{ {Port: 80}, }, IPs: []string{"10.42.42.42"}, }, } Expect(k8s.Create(ctx, &serviceImport)).To(Succeed()) }) It("updates service loadbalancer status with service import IPs", func() { var svcImport v1beta1.ServiceImport var s v1.Service Eventually(func() error { return k8s.Get(ctx, derivedServiceName, &s) }, 10).Should(Succeed()) Eventually(func() string { Expect(k8s.Get(ctx, serviceName, &svcImport)).To(Succeed()) if len(svcImport.Spec.IPs) > 0 { return svcImport.Spec.IPs[0] } return "" }, 10).Should(Equal(s.Status.LoadBalancer.Ingress[0].IP)) }, 15) }) }) ================================================ FILE: demo/.gitignore ================================================ *.kubeconfig *.tmp ================================================ FILE: demo/demo.sh ================================================ #!/bin/bash # Copyright 2020 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. # Resolve the absolute path to this script directory. cd $(dirname "${BASH_SOURCE[0]}") demo_dir=$(realpath "$(pwd)") scripts_dir=$(realpath "$(pwd)/../scripts") . "${demo_dir}/udemo.sh" . "${scripts_dir}/util.sh" DEMO_AUTO_RUN=true kubeconfig1="${KUBECONFIG1:-${scripts_dir}/c1.kubeconfig}" kubeconfig2="${KUBECONFIG2:-${scripts_dir}/c2.kubeconfig}" k1="kubectl --kubeconfig ${kubeconfig1}" k2="kubectl --kubeconfig ${kubeconfig2}" desc "Setup our demo namespace" run "${k1} create ns demo" run "${k2} create ns demo" c1_pane=$(tmux split-window -h -d -P) function cleanup() { tmux kill-pane -t "$c1_pane" } trap cleanup EXIT tmux send -t "$c1_pane" "${k1} logs -f mcs-api-controller" Enter desc "Create our service in each cluster" run "${k1} apply -f ${demo_dir}/yaml/dep1.yaml -f ${demo_dir}/yaml/svc.yaml" run "${k2} apply -f ${demo_dir}/yaml/dep2.yaml -f ${demo_dir}/yaml/svc.yaml" run "${k1} get endpointslice -n demo" desc "Lets look at some requests to the service in cluster 1" run "${k1} -n demo run -i --rm --restart=Never --image=jeremyot/request:0a40de8 request -- --duration=5s --address=serve.demo.svc.cluster.local" desc "Ok, looks normal. Let's import the service from our other cluster" ep_1=$(${k1} get endpointslice -n demo -l 'kubernetes.io/service-name=serve' --template="{{(index .items 0).metadata.name}}") ep_2=$(${k2} get endpointslice -n demo -l 'kubernetes.io/service-name=serve' --template="{{(index .items 0).metadata.name}}") run "${k1} get endpointslice -n demo ${ep_1} -o yaml | ${demo_dir}/edit-meta --metadata '{name: imported-${ep_1}, namespace: demo, labels: {multicluster.kubernetes.io/service-name: serve}}' > ${demo_dir}/yaml/slice-1.tmp" run "${k2} get endpointslice -n demo ${ep_2} -o yaml | ${demo_dir}/edit-meta --metadata '{name: imported-${ep_2}, namespace: demo, labels: {multicluster.kubernetes.io/service-name: serve}}' > ${demo_dir}/yaml/slice-2.tmp" run "${k1} apply -f ${demo_dir}/yaml/serviceimport.yaml -f ${demo_dir}/yaml/slice-1.tmp -f ${demo_dir}/yaml/slice-2.tmp" run "${k2} apply -f ${demo_dir}/yaml/serviceimport.yaml -f ${demo_dir}/yaml/slice-1.tmp -f ${demo_dir}/yaml/slice-2.tmp" run "${k1} apply -f ${demo_dir}/yaml/serviceimport-with-vip.yaml -f ${demo_dir}/yaml/slice-1.tmp -f ${demo_dir}/yaml/slice-2.tmp" run "${k2} apply -f ${demo_dir}/yaml/serviceimport-with-vip.yaml -f ${demo_dir}/yaml/slice-1.tmp -f ${demo_dir}/yaml/slice-2.tmp" desc "See what we've created..." run "${k1} get -n demo serviceimports" run "${k1} get -n demo endpointslice" run "${k1} get -n demo service" function import_ip() { ${k1} get serviceimport -n demo -o go-template --template='{{index (index .items 0).spec.ips 0}}' } waitfor import_ip vip=$(${k1} get serviceimport -n demo -o go-template --template='{{index (index .items 0).spec.ips 0}}') desc "Now grab the multi-cluster VIP from the serviceimport..." run "${k1} get serviceimport -n demo -o go-template --template='{{index (index .items 0).spec.ips 0}}{{\"\n\"}}'" desc "...and connect to it" run "${k1} -n demo run -i --rm --restart=Never --image=jeremyot/request:0a40de8 request -- --duration=10s --address=${vip}" run "${k1} -n demo run -i --rm --restart=Never --image=jeremyot/request:0a40de8 request -- --duration=10s --address=serve.demo.svc.clusterset.local" desc "We have a multi-cluster service!" desc "See for yourself" desc "Cluster 1: kubectl --kubeconfig ${kubeconfig1} -n demo" desc "Cluster 2: kubectl --kubeconfig ${kubeconfig2} -n demo" desc "(Enter to exit)" read -s ================================================ FILE: demo/edit-meta ================================================ #!/usr/bin/env python3 import sys import argparse import yaml if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-m', '--metadata', dest='metadata', help="Overwrite the resource's metadata with this value, may be any valid YAML") args = parser.parse_args() data = yaml.load(sys.stdin, Loader=yaml.Loader) if args.metadata: value = yaml.load(args.metadata, Loader=yaml.Loader) data['metadata'] = value print(yaml.dump(data, indent=2, default_flow_style=False, Dumper=yaml.Dumper)) ================================================ FILE: demo/reset.sh ================================================ #!/bin/bash # Copyright 2020 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. cd $(dirname ${BASH_SOURCE}) . ./udemo.sh c1=c1 c2=c2 k1="kubectl --kubeconfig ${c1}.kubeconfig" k2="kubectl --kubeconfig ${c2}.kubeconfig" ${k1} delete ns demo ${k2} delete ns demo ================================================ FILE: demo/udemo.sh ================================================ #!/bin/bash # Copyright 2016 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. readonly reset=$(tput sgr0) readonly green=$(tput bold; tput setaf 2) readonly yellow=$(tput bold; tput setaf 3) readonly blue=$(tput bold; tput setaf 6) readonly timeout=$(if [ "$(uname)" == "Darwin" ]; then echo "1"; else echo "0.1"; fi) function desc() { maybe_first_prompt echo "$blue# $@$reset" prompt } function prompt() { echo -n "$yellow\$ $reset" } started="" function maybe_first_prompt() { if [ -z "$started" ]; then prompt started=true fi } # After a `run` this variable will hold the stdout of the command that was run. # If the command was interactive, this will likely be garbage. DEMO_RUN_STDOUT="" function run() { maybe_first_prompt rate=25 if [ -n "$DEMO_RUN_FAST" ]; then rate=1000 fi echo "$green$1$reset" | pv -qL $rate if [ -n "$DEMO_RUN_FAST" ]; then sleep 0.5 fi OFILE="$(mktemp -t $(basename $0).XXXXXX)" if [[ "$OSTYPE" == "darwin"* ]]; then script -q -a "$OFILE" bash -c "$1" else script -eq -c "$1" -f "$OFILE" fi r=$? read -d '' -t "${timeout}" -n 10000 # clear stdin prompt if [ -z "$DEMO_AUTO_RUN" ]; then read -s fi DEMO_RUN_STDOUT="$(tail -n +2 $OFILE | sed 's/\r//g')" return $r } function relative() { for arg; do echo "$(realpath $(dirname $(which $0)))/$arg" | sed "s|$(realpath $(pwd))|.|" done } SSH_NODE=$(kubectl get nodes | tail -1 | cut -f1 -d' ') trap "echo" EXIT ================================================ FILE: demo/yaml/dep1.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: serve namespace: demo spec: replicas: 1 selector: matchLabels: app: serve template: metadata: labels: app: serve spec: containers: - name: serve image: jeremyot/serve:0a40de8 args: - "--message='hello from cluster 1 (Node: {{env \"NODE_NAME\"}} Pod: {{env \"POD_NAME\"}} Address: {{addr}})'" env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name ================================================ FILE: demo/yaml/dep2.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: serve namespace: demo spec: replicas: 1 selector: matchLabels: app: serve template: metadata: labels: app: serve spec: containers: - name: serve image: jeremyot/serve:0a40de8 args: - "--message='hello from cluster 2 (Node: {{env \"NODE_NAME\"}} Pod: {{env \"POD_NAME\"}} Address: {{addr}})'" env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name ================================================ FILE: demo/yaml/serviceimport-with-vip.yaml ================================================ apiVersion: multicluster.x-k8s.io/v1beta1 kind: ServiceImport metadata: name: serve-with-vip namespace: demo spec: type: ClusterSetIP ips: - 1.2.3.4 ports: - port: 80 protocol: TCP ================================================ FILE: demo/yaml/serviceimport.yaml ================================================ apiVersion: multicluster.x-k8s.io/v1beta1 kind: ServiceImport metadata: name: serve namespace: demo spec: type: ClusterSetIP ports: - port: 80 protocol: TCP ================================================ FILE: demo/yaml/svc.yaml ================================================ apiVersion: v1 kind: Service metadata: name: serve namespace: demo spec: ports: - port: 80 targetPort: 8080 selector: app: serve ================================================ FILE: e2e/connectivity_test.go ================================================ /* Copyright 2020 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 e2etest import ( "context" "fmt" "math/rand" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var ( replicaCount = int32(1) helloService = v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "hello", }, Spec: v1.ServiceSpec{ Selector: map[string]string{ "app": "hello", }, Ports: []v1.ServicePort{ { Name: "tcp", Port: 42, Protocol: v1.ProtocolTCP, }, { Name: "udp", Port: 42, Protocol: v1.ProtocolUDP, }, }, }, } helloServiceImport = v1beta1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Name: "hello", }, Spec: v1beta1.ServiceImportSpec{ Type: v1beta1.ClusterSetIP, Ports: []v1beta1.ServicePort{ { Name: "tcp", Port: 42, Protocol: v1.ProtocolTCP, }, { Name: "udp", Port: 42, Protocol: v1.ProtocolUDP, }, }, }, } helloDeployment = appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "hello", }, Spec: appsv1.DeploymentSpec{ Replicas: &replicaCount, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "hello", }, }, Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"app": "hello"}, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "hello-tcp", Image: "alpine/socat:1.7.4.4", Args: []string{"-v", "-v", "TCP-LISTEN:42,crlf,reuseaddr,fork", "SYSTEM:echo $(MY_POD_IP)"}, Env: []v1.EnvVar{ { Name: "MY_POD_IP", ValueFrom: &v1.EnvVarSource{ FieldRef: &v1.ObjectFieldSelector{ FieldPath: "status.podIP", }, }, }, }, }, { Name: "hello-udp", Image: "alpine/socat:1.7.4.4", Args: []string{"-v", "-v", "UDP-LISTEN:42,crlf,reuseaddr,fork", "SYSTEM:echo $(MY_POD_IP)"}, Env: []v1.EnvVar{ { Name: "MY_POD_IP", ValueFrom: &v1.EnvVarSource{ FieldRef: &v1.ObjectFieldSelector{ FieldPath: "status.podIP", }, }, }, }, }, }, }, }, }, } requestPod = v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "request", Labels: map[string]string{"app": "request"}, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "request", Image: "busybox", Args: []string{"/bin/sh", "-ec", "while :; do echo '.'; sleep 5 ; done"}, }, }, }, } ) var _ = Describe("Connectivity", func() { var ( namespace string ctx = context.Background() serviceImport *v1beta1.ServiceImport podIPs []string reqPod *v1.Pod verifyConnectivity = func() { checkPodReachable := func(command []string) { ips := map[string]int{} Eventually(func(g Gomega) { stdout, _, err := execCmd(cluster1.k8s, restcfg1, reqPod.Name, reqPod.Namespace, command) g.Expect(err).ToNot(HaveOccurred()) ip := strings.TrimSpace(string(stdout)) g.Expect(ip).To(BeElementOf(podIPs)) ips[ip]++ }, "5m").MustPassRepeatedly(50).Should(Succeed()) Expect(ips).To(HaveEach(Not(BeZero()))) } Specify("UDP connects across clusters using the VIP", func() { command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc -uw1 %s 42", serviceImport.Spec.IPs[0])} checkPodReachable(command) }) Specify("TCP connects across clusters using the VIP", func() { command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s 42", serviceImport.Spec.IPs[0])} checkPodReachable(command) }) Specify("UDP connects across clusters using the DNS name", func() { command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc -uw1 %s.%s.svc.clusterset.local 42", serviceImport.Name, serviceImport.Namespace)} checkPodReachable(command) }) Specify("TCP connects across clusters using the DNS name", func() { command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.clusterset.local 42", serviceImport.Name, serviceImport.Namespace)} checkPodReachable(command) }) } ) BeforeEach(func() { namespace = fmt.Sprintf("mcse2e-conformance-%v", rand.Uint32()) _, err := cluster1.k8s.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: namespace}, }, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) _, err = cluster2.k8s.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: namespace}, }, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) pod := requestPod _, err = cluster1.k8s.CoreV1().Pods(namespace).Create(ctx, &pod, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) dep := helloDeployment _, err = cluster2.k8s.AppsV1().Deployments(namespace).Create(ctx, &dep, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) svc := helloService _, err = cluster2.k8s.CoreV1().Services(namespace).Create(ctx, &svc, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) imp := helloServiceImport _, err = cluster1.mcs.MulticlusterV1beta1().ServiceImports(namespace).Create(ctx, &imp, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) Eventually(func() string { rp, err := cluster1.k8s.CoreV1().Pods(namespace).Get(ctx, requestPod.Name, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) return rp.Name }).ShouldNot(BeEmpty()) Eventually(func() string { pods, err := cluster2.k8s.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(helloDeployment.Spec.Selector), }) Expect(err).ToNot(HaveOccurred()) if len(pods.Items) > 0 { return pods.Items[0].Status.PodIP } return "" }, 30).ShouldNot(BeEmpty()) pods, err := cluster2.k8s.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(helloDeployment.Spec.Selector), }) Expect(err).ToNot(HaveOccurred()) for _, pod := range pods.Items { podIPs = append(podIPs, pod.Status.PodIP) } exportService(ctx, cluster2, cluster1, namespace, svc.Name) Eventually(func() []string { svcImport, err := cluster1.mcs.MulticlusterV1beta1().ServiceImports(namespace).Get(ctx, helloServiceImport.Name, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) return svcImport.Spec.IPs }).ShouldNot(BeEmpty()) serviceImport, err = cluster1.mcs.MulticlusterV1beta1().ServiceImports(namespace).Get(ctx, helloServiceImport.Name, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) reqPod, err = cluster1.k8s.CoreV1().Pods(namespace).Get(ctx, requestPod.Name, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) By("Created all in " + namespace) }) AfterEach(func() { if *noTearDown { By(fmt.Sprintf("Skipping teardown. Test namespace %q", namespace)) By(fmt.Sprintf("Cluster 1: kubectl --kubeconfig %q -n %q", *kubeconfig1, namespace)) By(fmt.Sprintf("Cluster 2: kubectl --kubeconfig %q -n %q", *kubeconfig2, namespace)) return } Expect(cluster1.k8s.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{})).To(Succeed()) Expect(cluster2.k8s.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{})).To(Succeed()) }) When("trying to reach a service exported from only the remote cluster", func() { verifyConnectivity() }) When("trying to reach a service exported by both the local and remote clusters", func() { BeforeEach(func() { dep := helloDeployment _, err := cluster1.k8s.AppsV1().Deployments(namespace).Create(ctx, &dep, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) svc := helloService _, err = cluster1.k8s.CoreV1().Services(namespace).Create(ctx, &svc, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) Eventually(func() string { pods, err := cluster1.k8s.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(helloDeployment.Spec.Selector), }) Expect(err).ToNot(HaveOccurred()) if len(pods.Items) > 0 { return pods.Items[0].Status.PodIP } return "" }, 30).ShouldNot(BeEmpty()) pods, err := cluster1.k8s.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(helloDeployment.Spec.Selector), }) Expect(err).ToNot(HaveOccurred()) for _, pod := range pods.Items { podIPs = append(podIPs, pod.Status.PodIP) } exportService(ctx, cluster1, cluster1, namespace, svc.Name) }) verifyConnectivity() }) }) ================================================ FILE: e2e/e2e_suite_test.go ================================================ /* Copyright 2020 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 e2etest import ( "bytes" "context" "flag" "math/rand" "os" "strconv" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes/scheme" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" mcsclient "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" ) var ( kubeconfig1 = flag.String("kubeconfig1", os.Getenv("KUBECONFIG1"), "The path to a kubeconfig for cluster 1") kubeconfig2 = flag.String("kubeconfig2", os.Getenv("KUBECONFIG2"), "The path to a kubeconfig for cluster 2") noTearDown = flag.Bool("no-tear-down", tryParseBool(os.Getenv("NO_TEAR_DOWN")), "Don't tear down after test (useful for debugging failures).") cluster1 clusterClients cluster2 clusterClients restcfg1, _ = clientcmd.BuildConfigFromFlags("", *kubeconfig1) //restcfg2, _ = clientcmd.BuildConfigFromFlags("", *kubeconfig2) ) func tryParseBool(s string) bool { b, _ := strconv.ParseBool(s) return b } type clusterClients struct { k8s kubernetes.Interface mcs mcsclient.Interface } func TestE2E(t *testing.T) { flag.Parse() RegisterFailHandler(Fail) RunSpecs(t, "E2E Suite") } var _ = BeforeSuite(func() { rand.Seed(GinkgoRandomSeed()) Expect(*kubeconfig1).ToNot(BeEmpty(), "either --kubeconfig1 or KUBECONFIG1 must be set") Expect(*kubeconfig2).ToNot(BeEmpty(), "either --kubeconfig2 or KUBECONFIG2 must be set") restcfg1, err := clientcmd.BuildConfigFromFlags("", *kubeconfig1) Expect(err).ToNot(HaveOccurred()) restcfg2, err := clientcmd.BuildConfigFromFlags("", *kubeconfig2) Expect(err).ToNot(HaveOccurred()) cluster1 = clusterClients{ k8s: kubernetes.NewForConfigOrDie(restcfg1), mcs: mcsclient.NewForConfigOrDie(restcfg1), } cluster2 = clusterClients{ k8s: kubernetes.NewForConfigOrDie(restcfg2), mcs: mcsclient.NewForConfigOrDie(restcfg2), } }) func execCmd(k8s kubernetes.Interface, config *restclient.Config, podName string, podNamespace string, command []string) ([]byte, []byte, error) { req := k8s.CoreV1().RESTClient().Post().Resource("pods").Name(podName).Namespace(podNamespace).SubResource("exec") req.VersionedParams(&v1.PodExecOptions{ Command: command, Stdin: false, Stdout: true, Stderr: true, TTY: true, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) if err != nil { return []byte{}, []byte{}, err } var stdout, stderr bytes.Buffer err = exec.Stream(remotecommand.StreamOptions{ Stdin: nil, Stdout: &stdout, Stderr: &stderr, }) if err != nil { return []byte{}, []byte{}, err } return stdout.Bytes(), stderr.Bytes(), nil } func exportService(ctx context.Context, fromCluster, toCluster clusterClients, namespace string, svcName string) { _, err := fromCluster.mcs.MulticlusterV1beta1().ServiceExports(namespace).Create(ctx, &v1beta1.ServiceExport{ ObjectMeta: metav1.ObjectMeta{ Name: svcName, }, }, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) var slices *discoveryv1.EndpointSliceList Eventually(func() int { eps := 0 slices, err = fromCluster.k8s.DiscoveryV1().EndpointSlices(namespace).List(ctx, metav1.ListOptions{ LabelSelector: labels.Set{discoveryv1.LabelServiceName: svcName}.AsSelector().String(), }) Expect(err).ToNot(HaveOccurred()) for _, s := range slices.Items { eps += len(s.Endpoints) } return eps }, 30).Should(Equal(1)) importedSlice := slices.Items[0] // This direct indexing is ok because we just asserted above that there is exactly one element here importedSlice.ObjectMeta = metav1.ObjectMeta{ GenerateName: svcName + "-", Labels: map[string]string{ v1beta1.LabelServiceName: svcName, }, } createdSlice, err := toCluster.k8s.DiscoveryV1().EndpointSlices(namespace).Create(ctx, &importedSlice, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) Eventually(func() string { updatedSlice, err := toCluster.k8s.DiscoveryV1().EndpointSlices(namespace).Get(ctx, createdSlice.Name, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) return updatedSlice.Labels[discoveryv1.LabelServiceName] }).ShouldNot(BeEmpty()) } ================================================ FILE: e2e/go.mod ================================================ module sigs.k8s.io/mcs-api/e2e go 1.23.0 require ( github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.35.1 k8s.io/api v0.32.5 k8s.io/apimachinery v0.32.5 k8s.io/client-go v0.32.5 sigs.k8s.io/mcs-api v0.3.0 ) replace sigs.k8s.io/mcs-api => .. require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) ================================================ FILE: e2e/go.sum ================================================ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk= k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64= k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM= k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0= k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= ================================================ FILE: e2e/localserviceimpact_test.go ================================================ /* Copyright 2024 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 e2etest import ( "context" "fmt" "math/rand" "strings" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = Describe("Local service not impacted", func() { helloDeployment := appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "hello", }, Spec: appsv1.DeploymentSpec{ Replicas: &replicaCount, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "hello", }, }, Template: v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"app": "hello"}, }, Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "hello-tcp", Image: "alpine/socat:1.7.4.4", Args: []string{"-v", "-v", "TCP-LISTEN:42,crlf,reuseaddr,fork", "SYSTEM:echo $(CLUSTER_ID)"}, Env: []v1.EnvVar{ { Name: "CLUSTER_ID", ValueFrom: &v1.EnvVarSource{ ConfigMapKeyRef: &v1.ConfigMapKeySelector{ LocalObjectReference: v1.LocalObjectReference{ Name: "cluster-info", }, Key: "clusterID", }, }, }, }, }, }, }, }, }, } var ( namespace string ctx = context.Background() reqPod *v1.Pod ) BeforeEach(func() { namespace = fmt.Sprintf("mcse2e-conformance-%v", rand.Uint32()) _, err := cluster1.k8s.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: namespace}, }, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) _, err = cluster2.k8s.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: namespace}, }, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) _, err = cluster1.k8s.CoreV1().ConfigMaps(namespace).Create(ctx, &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "cluster-info", }, Data: map[string]string{ "clusterID": "cluster1", }, }, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) _, err = cluster2.k8s.CoreV1().ConfigMaps(namespace).Create(ctx, &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "cluster-info", }, Data: map[string]string{ "clusterID": "cluster2", }, }, metav1.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) pod := requestPod _, err = cluster1.k8s.CoreV1().Pods(namespace).Create(ctx, &pod, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) dep := helloDeployment _, err = cluster1.k8s.AppsV1().Deployments(namespace).Create(ctx, &dep, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) _, err = cluster2.k8s.AppsV1().Deployments(namespace).Create(ctx, &dep, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) svc := helloService _, err = cluster1.k8s.CoreV1().Services(namespace).Create(ctx, &svc, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) _, err = cluster2.k8s.CoreV1().Services(namespace).Create(ctx, &svc, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) _, err = cluster1.mcs.MulticlusterV1beta1().ServiceImports(namespace).Create(ctx, &helloServiceImport, metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) deploymentAvailable := func(clients clusterClients) func(Gomega) { return func(g Gomega) { deployment, err := clients.k8s.AppsV1().Deployments(namespace).Get(ctx, dep.Name, metav1.GetOptions{}) g.Expect(err).NotTo(HaveOccurred()) g.Expect(deployment.Status.Conditions).To(ContainElement(Satisfy(func(cond appsv1.DeploymentCondition) bool { return cond.Type == appsv1.DeploymentAvailable && cond.Status == v1.ConditionTrue }))) } } Eventually(deploymentAvailable(cluster1), 30).Should(Succeed()) Eventually(deploymentAvailable(cluster2), 30).Should(Succeed()) exportService(ctx, cluster2, cluster1, namespace, svc.Name) Eventually(func() []string { svcImport, err := cluster1.mcs.MulticlusterV1beta1().ServiceImports(namespace).Get(ctx, helloServiceImport.Name, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) return svcImport.Spec.IPs }).ShouldNot(BeEmpty()) reqPod, err = cluster1.k8s.CoreV1().Pods(namespace).Get(ctx, requestPod.Name, metav1.GetOptions{}) Expect(err).ToNot(HaveOccurred()) By("Created all in " + namespace) }) AfterEach(func() { if *noTearDown { By(fmt.Sprintf("Skipping teardown. Test namespace %q", namespace)) By(fmt.Sprintf("Cluster 1: kubectl --kubeconfig %q -n %q", *kubeconfig1, namespace)) By(fmt.Sprintf("Cluster 2: kubectl --kubeconfig %q -n %q", *kubeconfig2, namespace)) return } Expect(cluster1.k8s.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{})).To(Succeed()) Expect(cluster2.k8s.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{})).To(Succeed()) }) Specify("DNS resolves as expected", func() { checkAllClustersReachable := func(command []string, clusterIDs ...string) { clusters := map[string]int{} Eventually(func(g Gomega) { stdout, _, err := execCmd(cluster1.k8s, restcfg1, reqPod.Name, reqPod.Namespace, command) g.Expect(err).ToNot(HaveOccurred()) clusterID := strings.TrimSpace(string(stdout)) g.Expect(clusterID).To(BeElementOf(clusterIDs)) clusters[clusterID]++ }).MustPassRepeatedly(20).Within(time.Second * 10).Should(Succeed()) Expect(clusters).To(HaveEach(Not(BeZero()))) } By("verifying a local, unexported service is reachable via cluster.local") command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.cluster.local 42", helloService.Name, namespace)} checkAllClustersReachable(command, "cluster1") By("verifying a remote, exported service is reachable via clusterset.local") command = []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.clusterset.local 42", helloServiceImport.Name, namespace)} checkAllClustersReachable(command, "cluster2") By("exporting the service from cluster1") exportService(ctx, cluster1, cluster1, namespace, helloService.Name) By("verifying a service exported from both local and remote clusters is reachable via clusterset.local") checkAllClustersReachable(command, "cluster1", "cluster2") By("verifying a local, exported service is reachable via cluster.local") command = []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.cluster.local 42", helloService.Name, namespace)} checkAllClustersReachable(command, "cluster1") }) }) ================================================ FILE: go.mod ================================================ module sigs.k8s.io/mcs-api go 1.23.0 require ( k8s.io/api v0.32.5 k8s.io/apimachinery v0.32.5 k8s.io/client-go v0.32.5 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk= k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64= k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM= k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0= k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= ================================================ FILE: hack/boilerplate/boilerplate.go.txt ================================================ /* Copyright YEAR 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. */ ================================================ FILE: hack/boilerplate/boilerplate.py ================================================ #!/usr/bin/env python3 # Copyright 2015 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. from __future__ import print_function import argparse import difflib import glob import json import mmap import os import re import sys from datetime import date parser = argparse.ArgumentParser() parser.add_argument( "filenames", help="list of files to check, all files if unspecified", nargs='*') rootdir = os.path.dirname(__file__) + "/../../" rootdir = os.path.abspath(rootdir) parser.add_argument( "--rootdir", default=rootdir, help="root directory to examine") default_boilerplate_dir = os.path.join(rootdir, "hack/boilerplate") parser.add_argument( "--boilerplate-dir", default=default_boilerplate_dir) parser.add_argument( "-v", "--verbose", help="give verbose output regarding why a file does not pass", action="store_true") args = parser.parse_args() verbose_out = sys.stderr if args.verbose else open("/dev/null", "w") def get_refs(): refs = {} for path in glob.glob(os.path.join(args.boilerplate_dir, "boilerplate.*.txt")): extension = os.path.basename(path).split(".")[1] ref_file = open(path, 'r') ref = ref_file.read().splitlines() ref_file.close() refs[extension] = ref return refs def file_passes(filename, refs, regexs): try: f = open(filename, 'r') except Exception as exc: print("Unable to open %s: %s" % (filename, exc), file=verbose_out) return False data = f.read() f.close() basename = os.path.basename(filename) extension = file_extension(filename) if extension != "": ref = refs[extension] else: ref = refs[basename] # remove build tags from the top of Go files if extension == "go": p = regexs["go_build_constraints"] (data, found) = p.subn("", data, 1) # remove shebang from the top of shell files if extension == "sh": p = regexs["shebang"] (data, found) = p.subn("", data, 1) data = data.splitlines() # if our test file is smaller than the reference it surely fails! if len(ref) > len(data): print('File %s smaller than reference (%d < %d)' % (filename, len(data), len(ref)), file=verbose_out) return False # trim our file to the same number of lines as the reference file data = data[:len(ref)] p = regexs["year"] for d in data: if p.search(d): print('File %s is missing the year' % filename, file=verbose_out) return False # Replace all occurrences of the regex "CURRENT_YEAR|...|2016|2015|2014" with "YEAR" p = regexs["date"] for i, d in enumerate(data): (data[i], found) = p.subn('YEAR', d) if found != 0: break # if we don't match the reference at this point, fail if ref != data: print("Header in %s does not match reference, diff:" % filename, file=verbose_out) if args.verbose: print(file=verbose_out) for line in difflib.unified_diff(ref, data, 'reference', filename, lineterm=''): print(line, file=verbose_out) print(file=verbose_out) return False return True def file_extension(filename): return os.path.splitext(filename)[1].split(".")[-1].lower() skipped_dirs = [ '.git', "vendor", "test/e2e/framework/framework.go", "images" ] def normalize_files(files): newfiles = [] for pathname in files: if any(x in pathname for x in skipped_dirs): continue newfiles.append(pathname) for i, pathname in enumerate(newfiles): if not os.path.isabs(pathname): newfiles[i] = os.path.join(args.rootdir, pathname) return newfiles def get_files(extensions): files = [] if len(args.filenames) > 0: files = args.filenames else: for root, dirs, walkfiles in os.walk(args.rootdir): # don't visit certain dirs. This is just a performance improvement # as we would prune these later in normalize_files(). But doing it # cuts down the amount of filesystem walking we do and cuts down # the size of the file list for d in skipped_dirs: if d in dirs: dirs.remove(d) for name in walkfiles: pathname = os.path.join(root, name) files.append(pathname) files = normalize_files(files) outfiles = [] for pathname in files: basename = os.path.basename(pathname) extension = file_extension(pathname) if extension in extensions or basename in extensions: outfiles.append(pathname) return outfiles def get_regexs(): regexs = {} # Search for "YEAR" which exists in the boilerplate, but shouldn't in the real thing regexs["year"] = re.compile('YEAR') # dates can be 2014, 2015, 2016, ..., CURRENT_YEAR, company holder names can be anything years = range(2014, date.today().year + 1) regexs["date"] = re.compile( '(%s)' % "|".join(map(lambda l: str(l), years))) # strip // +build \n\n build constraints regexs["go_build_constraints"] = re.compile( r"^(// ?(go:|\+)build.*\n)+\n", re.MULTILINE) # strip #!.* from shell scripts regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) return regexs def main(): regexs = get_regexs() refs = get_refs() filenames = get_files(refs.keys()) for filename in filenames: if not file_passes(filename, refs, regexs): print(filename, file=sys.stdout) return 0 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: hack/boilerplate/boilerplate.py.txt ================================================ #!/usr/bin/env python3 # Copyright YEAR 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. ================================================ FILE: hack/boilerplate/boilerplate.sh.txt ================================================ # Copyright YEAR 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. ================================================ FILE: hack/boilerplate.go.txt ================================================ /* Copyright 2020 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. */ ================================================ FILE: hack/kube-env.sh ================================================ #!/bin/bash # Copyright 2014 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. # Some useful colors. if [[ -z "${color_start-}" ]]; then declare -r color_start="\033[" declare -r color_red="${color_start}0;31m" declare -r color_yellow="${color_start}0;33m" declare -r color_green="${color_start}0;32m" declare -r color_norm="${color_start}0m" fi # Returns the server version as MMmmpp, with MM as the major # component, mm the minor component, and pp as the patch # revision. e.g. 0.7.1 is echoed as 701, and 1.0.11 would be # 10011. (This makes for easy integer comparison in bash.) function kube_server_version() { local server_version local major local minor local patch # This sed expression is the POSIX BRE to match strings like: # Server Version: &version.Info{Major:"0", Minor:"7+", GitVersion:"v0.7.0-dirty", GitCommit:"ad44234f7152e9c66bc2853575445c7071335e57", GitTreeState:"dirty"} # and capture the GitVersion portion (which has the patch level) server_version=$(${KUBECTL} --match-server-version=false version | grep "Server Version:") read major minor patch < <( echo ${server_version} | \ sed "s/.*GitVersion:\"v\([0-9]\{1,\}\)\.\([0-9]\{1,\}\)\.\([0-9]\{1,\}\).*/\1 \2 \3/") printf "%02d%02d%02d" ${major} ${minor} ${patch} | sed 's/^0*//' } ================================================ FILE: hack/update-codegen.sh ================================================ #!/usr/bin/env bash # Copyright 2020 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 SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. go -C tools install k8s.io/code-generator/cmd/{client-gen,lister-gen,informer-gen,deepcopy-gen,register-gen} # Go installs the above commands to get installed in $GOBIN if defined, and $GOPATH/bin otherwise: GOBIN="$(go env GOBIN)" gobin="${GOBIN:-$(go env GOPATH)/bin}" OUTPUT_PKG=sigs.k8s.io/mcs-api/pkg/client OUTPUT_DIR=$SCRIPT_ROOT/pkg/client FQ_APIS_V1ALPHA1=sigs.k8s.io/mcs-api/pkg/apis/v1alpha1 FQ_APIS_V1BETA1=sigs.k8s.io/mcs-api/pkg/apis/v1beta1 CLIENTSET_NAME=versioned CLIENTSET_PKG_NAME=clientset if [[ "${VERIFY_CODEGEN:-}" == "true" ]]; then echo "Running in verification mode" ORIG_OUTPUT_DIR="$OUTPUT_DIR" OUTPUT_DIR=$(mktemp -d) trap "rm -rf $OUTPUT_DIR" EXIT else # Clear existing code before re-generating it rm -rf "$OUTPUT_DIR" fi COMMON_FLAGS="--go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt" echo "Generating clientset at ${OUTPUT_PKG}/${CLIENTSET_PKG_NAME}" "${gobin}/client-gen" --clientset-name "${CLIENTSET_NAME}" --input-base "" --input "${FQ_APIS_V1ALPHA1}" --input "${FQ_APIS_V1BETA1}" --output-pkg "${OUTPUT_PKG}/${CLIENTSET_PKG_NAME}" --output-dir "$OUTPUT_DIR/$CLIENTSET_PKG_NAME" ${COMMON_FLAGS} echo "Generating listers at ${OUTPUT_PKG}/listers" "${gobin}/lister-gen" "${FQ_APIS_V1ALPHA1}" "${FQ_APIS_V1BETA1}" --output-pkg "${OUTPUT_PKG}/listers" --output-dir "${OUTPUT_DIR}/listers" ${COMMON_FLAGS} echo "Generating informers at ${OUTPUT_PKG}/informers" "${gobin}/informer-gen" \ "${FQ_APIS_V1ALPHA1}" "${FQ_APIS_V1BETA1}" \ --versioned-clientset-package "${OUTPUT_PKG}/${CLIENTSET_PKG_NAME}/${CLIENTSET_NAME}" \ --listers-package "${OUTPUT_PKG}/listers" \ --output-pkg "${OUTPUT_PKG}/informers" \ --output-dir "${OUTPUT_DIR}/informers" \ ${COMMON_FLAGS} echo "Generating register at ${FQ_APIS_V1ALPHA1} & ${FQ_APIS_V1BETA1}" "${gobin}/register-gen" "${FQ_APIS_V1ALPHA1}" "${FQ_APIS_V1BETA1}" --output-file zz_generated.register.go ${COMMON_FLAGS} if [[ "${VERIFY_CODEGEN:-}" == "true" ]]; then diff -urN "$ORIG_OUTPUT_DIR" "$OUTPUT_DIR" fi ================================================ FILE: hack/update-k8s.sh ================================================ #!/usr/bin/env bash # Copyright 2025 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. # This script updates mcs-api dependencies to either the latest # k8s.io dependencies, or to the version given as argument # (with the @, e.g. @v0.32.5). set -o errexit set -o nounset set -o pipefail SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. for dir in "$SCRIPT_ROOT"{,/tools}; do awk '/[^.]k8s.io[/][^ ]+ v[.0-9]+$/ { print $1 "'"$1"'" }' "$dir/go.mod" | xargs -r -n 1 go -C "$dir" get done for mod in "$SCRIPT_ROOT"/go.mod "$SCRIPT_ROOT"/*/go.mod; do go -C "${mod%/*}" mod tidy done make generate ================================================ FILE: hack/verify-all.sh ================================================ #!/bin/bash # Copyright 2014 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 SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. source "${SCRIPT_ROOT}/hack/kube-env.sh" SILENT=true function is-excluded { for e in $EXCLUDE; do if [[ $1 -ef ${BASH_SOURCE} ]]; then return fi if [[ $1 -ef "$SCRIPT_ROOT/hack/$e" ]]; then return fi done return 1 } while getopts ":v" opt; do case $opt in v) SILENT=false ;; \?) echo "Invalid flag: -$OPTARG" >&2 exit 1 ;; esac done if $SILENT ; then echo "Running in the silent mode, run with -v if you want to see script logs." fi EXCLUDE="verify-all.sh" ret=0 for t in "$SCRIPT_ROOT"/hack/verify-*.sh do if is-excluded $t ; then echo "Skipping $t" continue fi if $SILENT ; then echo -e "Verifying $t" if bash "$t" &> /dev/null; then echo -e "${color_green}SUCCESS${color_norm}" else echo -e "${color_red}FAILED${color_norm}" ret=1 fi else bash "$t" || ret=1 fi done exit $ret # ex: ts=2 sw=2 et filetype=sh ================================================ FILE: hack/verify-boilerplate.sh ================================================ #!/bin/bash # Copyright 2014 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 SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. boilerDir="${SCRIPT_ROOT}/hack/boilerplate" boiler="${boilerDir}/boilerplate.py" files_need_boilerplate=($(${boiler} "$@")) # Run boilerplate check if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then for file in "${files_need_boilerplate[@]}"; do echo "Boilerplate header is wrong for: ${file}" done exit 1 fi ================================================ FILE: hack/verify-codegen.sh ================================================ #!/bin/bash # Copyright 2020 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 SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. cd $SCRIPT_ROOT VERIFY_CODEGEN=true $SCRIPT_ROOT/hack/update-codegen.sh ================================================ FILE: hack/verify-crd-bump-revision.sh ================================================ #!/bin/bash # Copyright 2025 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. # Check against master if PULL_BASE_SHA is not defined by prow BASE_REF="${PULL_BASE_SHA:-master}" crd_changed="$(git diff --name-only "${BASE_REF}" | grep -c "^config/crd/.*\.yaml$")" version_label_changed="$(git diff -U0 "${BASE_REF}" -- "config/crd-base/" | grep -Ec "multicluster.x-k8s.io/(release-version|crd-schema-revision)")" if [ "${crd_changed}" -gt 0 ] && [ "${version_label_changed}" -lt 4 ]; then echo "❌ CRDs were modified, but the CRD version/revision labels were not changed in 'config/crd-base/'. Please update them." exit 1 fi ================================================ FILE: hack/verify-crds.sh ================================================ #!/bin/bash # Copyright 2020 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 SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. DIFFROOT="${SCRIPT_ROOT}/config" TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/config" _tmp="${SCRIPT_ROOT}/_tmp" # The controller-gen command for generating CRDs from API definitions. CONTROLLER_GEN="go -C tools run sigs.k8s.io/controller-tools/cmd/controller-gen" # Need v1 to support defaults in CRDs, unfortunately limiting us to k8s 1.16+ CRD_OPTIONS="crd:crdVersions=v1" cd "${SCRIPT_ROOT}" cleanup() { rm -rf "${_tmp}" } trap "cleanup" EXIT SIGINT cleanup mkdir -p "${TMP_DIFFROOT}" cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}" ${CONTROLLER_GEN} ${CRD_OPTIONS} rbac:roleName=mcs-derived-service-manager webhook \ paths="${SCRIPT_ROOT}/..." schemapatch:manifests="${SCRIPT_ROOT}/config/crd-base" output:crd:none \ output:schemapatch:dir="${TMP_DIFFROOT}/crd" output:rbac:dir="${TMP_DIFFROOT}/rbac" echo "diffing ${DIFFROOT} against freshly generated codegen in ${TMP_DIFFROOT}" ret=0 diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? if [[ $ret -eq 0 ]] then echo "${DIFFROOT} up to date." else echo "${DIFFROOT} is out of date. Please run 'make manifests'" exit 1 fi ================================================ FILE: hack/verify-gofmt.sh ================================================ #!/bin/bash # Copyright 2014 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. # GoFmt apparently is changing @ head... set -o errexit set -o nounset set -o pipefail KUBE_ROOT=$(dirname "${BASH_SOURCE}")/.. cd "${KUBE_ROOT}" find_files() { find . -not \( \ \( \ -wholename './.git' \ -o -wholename '*/vendor/*' \ \) -prune \ \) -name '*.go' } GOFMT="gofmt -s" bad_files=$(find_files | xargs $GOFMT -l) if [[ -n "${bad_files}" ]]; then echo "!!! '$GOFMT' needs to be run on the following files: " echo "${bad_files}" exit 1 fi ================================================ FILE: hack/verify-golint.sh ================================================ #!/bin/bash # Copyright 2014 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 KUBE_ROOT=$(dirname "${BASH_SOURCE}")/.. cd "${KUBE_ROOT}" PACKAGES=($(go list ./... | sed sXsigs.k8s.io/mcs-apiX..X)) bad_files=() for package in "${PACKAGES[@]}"; do out=$(go -C tools run golang.org/x/lint/golint -min_confidence=0.9 "${package}" 2>&1 | sed 'sX^../XX;/should not use dot imports/d;/exported const OptionalLabel/d;/^go: downloading/d' ||:) if [[ -n "${out}" ]]; then bad_files+=("${out}") fi done if [[ "${#bad_files[@]}" -ne 0 ]]; then echo "!!! golint problems: " for err in "${bad_files[@]}"; do echo "$err" done exit 1 fi # ex: ts=2 sw=2 et filetype=sh ================================================ FILE: pkg/apis/v1alpha1/BUILD ================================================ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ "doc.go", "register.go", "types.go", "well_known_labels.go", "zz_generated.deepcopy.go", ], importmap = "k8s.io/kubernetes/vendor/k8s.io/mcs-api/pkg/apis/v1alpha1", importpath = "k8s.io/mcs-api/pkg/apis/v1alpha1", visibility = ["//visibility:public"], deps = [ "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", ], ) filegroup( name = "package-srcs", srcs = glob(["**"]), tags = ["automanaged"], visibility = ["//visibility:private"], ) filegroup( name = "all-srcs", srcs = [":package-srcs"], tags = ["automanaged"], visibility = ["//visibility:public"], ) ================================================ FILE: pkg/apis/v1alpha1/doc.go ================================================ /* Copyright 2020 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 v1alpha1 contains API schema definitions for the Multi-Cluster // Services v1alpha1 API group. // +kubebuilder:object:generate=true // +groupName=multicluster.x-k8s.io package v1alpha1 ================================================ FILE: pkg/apis/v1alpha1/serviceexport.go ================================================ /* Copyright 2020 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 v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( // ServiceExportPluralName is the plural name of ServiceExport ServiceExportPluralName = "serviceexports" // ServiceExportKindName is the kind name of ServiceExport ServiceExportKindName = "ServiceExport" // ServiceExportFullName is the full name of ServiceExport ServiceExportFullName = ServiceExportPluralName + "." + GroupName ) // ServiceExportVersionedName is the versioned name of ServiceExport var ServiceExportVersionedName = ServiceExportKindName + "/" + GroupVersion.Version // +genclient // +kubebuilder:object:root=true // +kubebuilder:resource:shortName={svcex,svcexport} // ServiceExport declares that the Service with the same name and namespace // as this export should be consumable from other clusters. type ServiceExport struct { metav1.TypeMeta `json:",inline"` // +optional metav1.ObjectMeta `json:"metadata,omitempty"` // spec defines the behavior of a ServiceExport. // +optional Spec ServiceExportSpec `json:"spec,omitempty"` // status describes the current state of an exported service. // Service configuration comes from the Service that had the same // name and namespace as this ServiceExport. // Populated by the multi-cluster service implementation's controller. // +optional Status ServiceExportStatus `json:"status,omitempty"` } // ServiceExportSpec describes an exported service extra information type ServiceExportSpec struct { // exportedLabels describes the labels exported. It is optional for implementation. // +optional ExportedLabels map[string]string `json:"exportedLabels,omitempty"` // exportedAnnotations describes the annotations exported. It is optional for implementation. // +optional ExportedAnnotations map[string]string `json:"exportedAnnotations,omitempty"` } // ServiceExportStatus contains the current status of an export. type ServiceExportStatus struct { // +optional // +patchStrategy=merge // +patchMergeKey=type // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } const ( // ServiceExportValid means that the service referenced by this // service export has been recognized as valid by an mcs-controller. // This will be false if the service is found to be unexportable // (ExternalName, not found). // // Deprecated: use ServiceExportConditionValid instead ServiceExportValid = "Valid" // ServiceExportConflict means that there is a conflict between two // exports for the same Service. When "True", the condition message // should contain enough information to diagnose the conflict: // field(s) under contention, which cluster won, and why. // Users should not expect detailed per-cluster information in the // conflict message. // // Deprecated: use ServiceExportConditionConflict instead ServiceExportConflict = "Conflict" ) // +kubebuilder:object:root=true // ServiceExportList represents a list of endpoint slices type ServiceExportList struct { metav1.TypeMeta `json:",inline"` // Standard list metadata. // +optional metav1.ListMeta `json:"metadata,omitempty"` // List of endpoint slices // +listType=set Items []ServiceExport `json:"items"` } // ServiceExportConditionType is a type of condition associated with a // ServiceExport. This type should be used with the ServiceExportStatus.Conditions // field. type ServiceExportConditionType string // ServiceExportConditionReason defines the set of reasons that explain why a // particular ServiceExport condition type has been raised. type ServiceExportConditionReason string // NewServiceExportCondition creates a new ServiceExport condition func NewServiceExportCondition(t ServiceExportConditionType, status metav1.ConditionStatus, reason ServiceExportConditionReason, msg string) metav1.Condition { return metav1.Condition{ Type: string(t), Status: status, Reason: string(reason), Message: msg, LastTransitionTime: metav1.Now(), } } const ( // ServiceExportConditionValid is true when the Service Export is valid. // This does not indicate whether or not the configuration has been exported // to a control plane / data plane. // // // Possible reasons for this condition to be true are: // // * "Valid" // // Possible reasons for this condition to be False are: // // * "NoService" // * "InvalidServiceType" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. ServiceExportConditionValid ServiceExportConditionType = "Valid" // ServiceExportReasonValid is used with the "Valid" condition when the // condition is True. ServiceExportReasonValid ServiceExportConditionReason = "Valid" // ServiceExportReasonNoService is used with the "Valid" condition when // the associated Service does not exist. ServiceExportReasonNoService ServiceExportConditionReason = "NoService" // ServiceExportReasonInvalidServiceType is used with the "Valid" // condition when the associated Service has an invalid type // (per the KEP at least the ExternalName type). ServiceExportReasonInvalidServiceType ServiceExportConditionReason = "InvalidServiceType" ) const ( // ServiceExportConditionReady is true when the service is exported // to some control plane or data plane or ready to be pulled. // // // Possible reasons for this condition to be true are: // // * "Exported" // * "Ready" // // Possible reasons for this condition to be False are: // // * "Pending" // * "Failed" // // Possible reasons for this condition to be Unknown are: // // * "Pending" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. ServiceExportConditionReady ServiceExportConditionType = "Ready" // ServiceExportReasonExported is used with the "Ready" condition // when the condition is True and the service has been exported. // This would be used when an implementation exports a service // to a control plane or data plane. ServiceExportReasonExported ServiceExportConditionReason = "Exported" // ServiceExportReasonReady is used with the "Ready" condition // when the condition is True and the service has been exported. // This would typically be used in an implementation that uses a // pull model. ServiceExportReasonReady ServiceExportConditionReason = "Ready" // ServiceExportReasonPending is used with the "Ready" condition // when the service is in the process of being exported. ServiceExportReasonPending ServiceExportConditionReason = "Pending" // ServiceExportReasonFailed is used with the "Ready" condition // when the service failed to be exported with the message providing // the specific reason. ServiceExportReasonFailed ServiceExportConditionReason = "Failed" ) const ( // ServiceExportConditionConflict indicates that some property of an // exported service has conflicting values across the constituent // ServiceExports. This condition must be at least raised on the // conflicting ServiceExport and is recommended to be raised on all on // all the constituent ServiceExports if feasible. // // // Possible reasons for this condition to be true are: // // * "PortConflict" // * "TypeConflict" // * "SessionAffinityConflict" // * "SessionAffinityConfigConflict" // * "AnnotationsConflict" // * "LabelsConflict" // // When multiple conflicts occurs the above reasons may be combined // using commas. // // Possible reasons for this condition to be False are: // // * "NoConflicts" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. ServiceExportConditionConflict ServiceExportConditionType = "Conflict" // ServiceExportReasonPortConflict is used with the "Conflict" condition // when the exported service has a conflict related to port configuration // if the ports are not identical in all the constituent Services. ServiceExportReasonPortConflict ServiceExportConditionReason = "PortConflict" // ServiceExportReasonTypeConflict is used with the "Conflict" condition // when the exported service has a conflict related to the service type // (eg headless vs non-headless). ServiceExportReasonTypeConflict ServiceExportConditionReason = "TypeConflict" // ServiceExportReasonSessionAffinityConflict is used with the "Conflict" // condition when the exported service has a conflict related to session affinity. ServiceExportReasonSessionAffinityConflict ServiceExportConditionReason = "SessionAffinityConflict" // ServiceExportReasonSessionAffinityConfigConflict is used with the // "Conflict" condition when the exported service has a conflict related // to session affinity config. ServiceExportReasonSessionAffinityConfigConflict ServiceExportConditionReason = "SessionAffinityConfigConflict" // ServiceExportReasonLabelsConflict is used with the "Conflict" // condition when the ServiceExport has a conflict related to exported // labels. ServiceExportReasonLabelsConflict ServiceExportConditionReason = "LabelsConflict" // ServiceExportReasonAnnotationsConflict is used with the "Conflict" // condition when the ServiceExport has a conflict related to exported // annotations. ServiceExportReasonAnnotationsConflict ServiceExportConditionReason = "AnnotationsConflict" // ServiceExportReasonInternalTrafficPolicyConflict is used with the "Conflict" // condition when the exported service has a conflict related to internal traffic policy. ServiceExportReasonInternalTrafficPolicyConflict ServiceExportConditionReason = "InternalTrafficPolicyConflict" // ServiceExportReasonTrafficDistributionConflict is used with the "Conflict" // condition when the exported service has a conflict related to traffic distribution. ServiceExportReasonTrafficDistributionConflict ServiceExportConditionReason = "TrafficDistributionConflict" // ServiceExportReasonIPFamilyConflict is used with the "Conflict" condition // when the exported service has a conflict related to IPFamilies. // The handling of IP families is implementation-specific but this condition // must be used if a conflicting IP family may result in network traffic reaching // only a subset of the backends depending on the IP protocol used. ServiceExportReasonIPFamilyConflict ServiceExportConditionReason = "IPFamilyConflict" // ServiceExportReasonNoConflicts is used with the "Conflict" condition // when the condition is False. ServiceExportReasonNoConflicts ServiceExportConditionReason = "NoConflicts" ) ================================================ FILE: pkg/apis/v1alpha1/serviceimport.go ================================================ /* Copyright 2020 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 v1alpha1 import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( // ServiceImportPluralName is the plural name of ServiceImport ServiceImportPluralName = "serviceimports" // ServiceImportKindName is the kind name of ServiceImport ServiceImportKindName = "ServiceImport" // ServiceImportFullName is the full name of ServiceImport ServiceImportFullName = ServiceImportPluralName + "." + GroupName ) // ServiceImportVersionedName is the versioned name of ServiceImport var ServiceImportVersionedName = ServiceImportKindName + "/" + GroupVersion.Version // +genclient // +kubebuilder:object:root=true // +kubebuilder:resource:shortName={svcim,svcimport} // ServiceImport describes a service imported from clusters in a ClusterSet. type ServiceImport struct { metav1.TypeMeta `json:",inline"` // +optional metav1.ObjectMeta `json:"metadata,omitempty"` // spec defines the behavior of a ServiceImport. // +optional Spec ServiceImportSpec `json:"spec,omitempty"` // status contains information about the exported services that form // the multi-cluster service referenced by this ServiceImport. // +optional Status ServiceImportStatus `json:"status,omitempty"` } // ServiceImportType designates the type of a ServiceImport type ServiceImportType string const ( // ClusterSetIP are only accessible via the ClusterSet IP. ClusterSetIP ServiceImportType = "ClusterSetIP" // Headless services allow backend pods to be addressed directly. Headless ServiceImportType = "Headless" ) // ServiceImportSpec describes an imported service and the information necessary to consume it. type ServiceImportSpec struct { // +listType=atomic Ports []ServicePort `json:"ports"` // ip will be used as the VIP for this service when type is ClusterSetIP. // +kubebuilder:validation:MaxItems:=2 // +optional IPs []string `json:"ips,omitempty"` // type defines the type of this service. // Must be ClusterSetIP or Headless. // +kubebuilder:validation:Enum=ClusterSetIP;Headless Type ServiceImportType `json:"type"` // Supports "ClientIP" and "None". Used to maintain session affinity. // Enable client IP based session affinity. // Must be ClientIP or None. // Defaults to None. // Ignored when type is Headless // More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies // +optional SessionAffinity v1.ServiceAffinity `json:"sessionAffinity,omitempty"` // sessionAffinityConfig contains session affinity configuration. // +optional SessionAffinityConfig *v1.SessionAffinityConfig `json:"sessionAffinityConfig,omitempty"` // IPFamilies identifies all the IPFamilies assigned for this ServiceImport. // +kubebuilder:validation:MaxItems:=2 // +optional IPFamilies []v1.IPFamily `json:"ipFamilies,omitempty"` // InternalTrafficPolicy describes how nodes distribute service traffic they // receive on the ClusterIP. If set to "Local", the proxy will assume that pods // only want to talk to endpoints of the service on the same node as the pod, // dropping the traffic if there are no local endpoints. The default value, // "Cluster", uses the standard behavior of routing to all endpoints evenly // (possibly modified by topology and other features). // +optional InternalTrafficPolicy *v1.ServiceInternalTrafficPolicy `json:"internalTrafficPolicy,omitempty"` // TrafficDistribution offers a way to express preferences for how traffic // is distributed to Service endpoints. Implementations can use this field // as a hint, but are not required to guarantee strict adherence. If the // field is not set, the implementation will apply its default routing // strategy. If set to "PreferClose", implementations should prioritize // endpoints that are in the same zone. // +optional TrafficDistribution *string `json:"trafficDistribution,omitempty"` } // ServicePort represents the port on which the service is exposed type ServicePort struct { // The name of this port within the service. This must be a DNS_LABEL. // All ports within a ServiceSpec must have unique names. When considering // the endpoints for a Service, this must match the 'name' field in the // EndpointPort. // Optional if only one ServicePort is defined on this service. // +optional Name string `json:"name,omitempty"` // The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". // Default is TCP. // +optional Protocol v1.Protocol `json:"protocol,omitempty"` // The application protocol for this port. // This is used as a hint for implementations to offer richer behavior for protocols that they understand. // This field follows standard Kubernetes label syntax. // Valid values are either: // // * Un-prefixed protocol names - reserved for IANA standard service names (as per // RFC-6335 and https://www.iana.org/assignments/service-names). // // * Kubernetes-defined prefixed names: // * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 // // * Other protocols should use implementation-defined prefixed names such as // mycompany.com/my-custom-protocol. // Field can be enabled with ServiceAppProtocol feature gate. // +optional AppProtocol *string `json:"appProtocol,omitempty"` // The port that will be exposed by this service. Port int32 `json:"port"` } // ServiceImportStatus describes derived state of an imported service. type ServiceImportStatus struct { // clusters is the list of exporting clusters from which this service // was derived. // +optional // +patchStrategy=merge // +patchMergeKey=cluster // +listType=map // +listMapKey=cluster Clusters []ClusterStatus `json:"clusters,omitempty"` // +optional // +patchStrategy=merge // +patchMergeKey=type // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // ClusterStatus contains service configuration mapped to a specific source cluster type ClusterStatus struct { // cluster is the name of the exporting cluster. Must be a valid RFC-1123 DNS // label. Cluster string `json:"cluster"` } // +kubebuilder:object:root=true // ServiceImportList represents a list of endpoint slices type ServiceImportList struct { metav1.TypeMeta `json:",inline"` // Standard list metadata. // +optional metav1.ListMeta `json:"metadata,omitempty"` // List of endpoint slices // +listType=set Items []ServiceImport `json:"items"` } // ServiceImportConditionType is a type of condition associated with a // ServiceImport. This type should be used with the ServiceImportStatus.Conditions // field. type ServiceImportConditionType string // ServiceImportConditionReason defines the set of reasons that explain why a // particular ServiceImport condition type has been raised. type ServiceImportConditionReason string // NewServiceImportCondition creates a new ServiceImport condition func NewServiceImportCondition(t ServiceImportConditionType, status metav1.ConditionStatus, reason ServiceImportConditionReason, msg string) metav1.Condition { return metav1.Condition{ Type: string(t), Status: status, Reason: string(reason), Message: msg, LastTransitionTime: metav1.Now(), } } const ( // ServiceImportConditionReady is true when the Service Import is ready. // // // Possible reasons for this condition to be true are: // // * "Ready" // // Possible reasons for this condition to be False are: // // * "Pending" // * "IPFamilyNotSupported" // // Possible reasons for this condition to be Unknown are: // // * "Pending" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. ServiceImportConditionReady ServiceImportConditionType = "Ready" // ServiceImportReasonReady is used with the "Ready" condition when the // condition is True. ServiceImportReasonReady ServiceImportConditionReason = "Ready" // ServiceImportReasonPending is used with the "Ready" condition when // the ServiceImport is in the process of being created or updated. ServiceImportReasonPending ServiceImportConditionReason = "Pending" // ServiceImportReasonIPFamilyNotSupported is used with the "Ready" // condition when the service can not be imported due to IP families // mismatch. ServiceImportReasonIPFamilyNotSupported ServiceImportConditionReason = "IPFamilyNotSupported" ) ================================================ FILE: pkg/apis/v1alpha1/well_known_labels.go ================================================ /* Copyright 2020 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 v1alpha1 const ( // LabelServiceName is used to indicate the name of multi-cluster service // that an EndpointSlice belongs to. LabelServiceName = "multicluster.kubernetes.io/service-name" // LabelSourceCluster is used to indicate the name of the cluster in which an exported resource exists. LabelSourceCluster = "multicluster.kubernetes.io/source-cluster" ) ================================================ FILE: pkg/apis/v1alpha1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2020 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. */ // Code generated by controller-gen. DO NOT EDIT. package v1alpha1 import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = *in } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. func (in *ClusterStatus) DeepCopy() *ClusterStatus { if in == nil { return nil } out := new(ClusterStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExport) DeepCopyInto(out *ServiceExport) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExport. func (in *ServiceExport) DeepCopy() *ServiceExport { if in == nil { return nil } out := new(ServiceExport) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ServiceExport) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExportList) DeepCopyInto(out *ServiceExportList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]ServiceExport, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExportList. func (in *ServiceExportList) DeepCopy() *ServiceExportList { if in == nil { return nil } out := new(ServiceExportList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ServiceExportList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExportSpec) DeepCopyInto(out *ServiceExportSpec) { *out = *in if in.ExportedLabels != nil { in, out := &in.ExportedLabels, &out.ExportedLabels *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.ExportedAnnotations != nil { in, out := &in.ExportedAnnotations, &out.ExportedAnnotations *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExportSpec. func (in *ServiceExportSpec) DeepCopy() *ServiceExportSpec { if in == nil { return nil } out := new(ServiceExportSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExportStatus) DeepCopyInto(out *ServiceExportStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExportStatus. func (in *ServiceExportStatus) DeepCopy() *ServiceExportStatus { if in == nil { return nil } out := new(ServiceExportStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceImport) DeepCopyInto(out *ServiceImport) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImport. func (in *ServiceImport) DeepCopy() *ServiceImport { if in == nil { return nil } out := new(ServiceImport) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ServiceImport) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceImportList) DeepCopyInto(out *ServiceImportList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]ServiceImport, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImportList. func (in *ServiceImportList) DeepCopy() *ServiceImportList { if in == nil { return nil } out := new(ServiceImportList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ServiceImportList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceImportSpec) DeepCopyInto(out *ServiceImportSpec) { *out = *in if in.Ports != nil { in, out := &in.Ports, &out.Ports *out = make([]ServicePort, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.IPs != nil { in, out := &in.IPs, &out.IPs *out = make([]string, len(*in)) copy(*out, *in) } if in.SessionAffinityConfig != nil { in, out := &in.SessionAffinityConfig, &out.SessionAffinityConfig *out = new(corev1.SessionAffinityConfig) (*in).DeepCopyInto(*out) } if in.IPFamilies != nil { in, out := &in.IPFamilies, &out.IPFamilies *out = make([]corev1.IPFamily, len(*in)) copy(*out, *in) } if in.InternalTrafficPolicy != nil { in, out := &in.InternalTrafficPolicy, &out.InternalTrafficPolicy *out = new(corev1.ServiceInternalTrafficPolicy) **out = **in } if in.TrafficDistribution != nil { in, out := &in.TrafficDistribution, &out.TrafficDistribution *out = new(string) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImportSpec. func (in *ServiceImportSpec) DeepCopy() *ServiceImportSpec { if in == nil { return nil } out := new(ServiceImportSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceImportStatus) DeepCopyInto(out *ServiceImportStatus) { *out = *in if in.Clusters != nil { in, out := &in.Clusters, &out.Clusters *out = make([]ClusterStatus, len(*in)) copy(*out, *in) } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImportStatus. func (in *ServiceImportStatus) DeepCopy() *ServiceImportStatus { if in == nil { return nil } out := new(ServiceImportStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServicePort) DeepCopyInto(out *ServicePort) { *out = *in if in.AppProtocol != nil { in, out := &in.AppProtocol, &out.AppProtocol *out = new(string) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePort. func (in *ServicePort) DeepCopy() *ServicePort { if in == nil { return nil } out := new(ServicePort) in.DeepCopyInto(out) return out } ================================================ FILE: pkg/apis/v1alpha1/zz_generated.register.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright 2020 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. */ // Code generated by register-gen. DO NOT EDIT. package v1alpha1 import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" ) // GroupName specifies the group name used to register the objects. const GroupName = "multicluster.x-k8s.io" // GroupVersion specifies the group and the version used to register the objects. var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1alpha1"} // SchemeGroupVersion is group version used to register these objects // Deprecated: use GroupVersion instead. var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder // Deprecated: use Install instead AddToScheme = localSchemeBuilder.AddToScheme Install = localSchemeBuilder.AddToScheme ) func init() { // We only register manually written functions here. The registration of the // generated functions takes place in the generated files. The separation // makes the code compile even when the generated files are missing. localSchemeBuilder.Register(addKnownTypes) } // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &ServiceExport{}, &ServiceExportList{}, &ServiceImport{}, &ServiceImportList{}, ) // AddToGroupVersion allows the serialization of client types like ListOptions. v1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ================================================ FILE: pkg/apis/v1beta1/BUILD ================================================ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", srcs = [ "doc.go", "register.go", "types.go", "well_known_labels.go", "zz_generated.deepcopy.go", ], importmap = "k8s.io/kubernetes/vendor/k8s.io/mcs-api/pkg/apis/v1beta1", importpath = "k8s.io/mcs-api/pkg/apis/v1beta1", visibility = ["//visibility:public"], deps = [ "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", ], ) filegroup( name = "package-srcs", srcs = glob(["**"]), tags = ["automanaged"], visibility = ["//visibility:private"], ) filegroup( name = "all-srcs", srcs = [":package-srcs"], tags = ["automanaged"], visibility = ["//visibility:public"], ) ================================================ FILE: pkg/apis/v1beta1/doc.go ================================================ /* Copyright 2025 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 v1beta1 contains API schema definitions for the Multi-Cluster // Services v1beta1 API group. // +kubebuilder:object:generate=true // +groupName=multicluster.x-k8s.io package v1beta1 ================================================ FILE: pkg/apis/v1beta1/serviceexport.go ================================================ /* Copyright 2025 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 v1beta1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( // ServiceExportPluralName is the plural name of ServiceExport ServiceExportPluralName = "serviceexports" // ServiceExportKindName is the kind name of ServiceExport ServiceExportKindName = "ServiceExport" // ServiceExportFullName is the full name of ServiceExport ServiceExportFullName = ServiceExportPluralName + "." + GroupName ) // ServiceExportVersionedName is the versioned name of ServiceExport var ServiceExportVersionedName = ServiceExportKindName + "/" + GroupVersion.Version // +genclient // +kubebuilder:object:root=true // +kubebuilder:resource:shortName={svcex,svcexport} // +kubebuilder:storageversion // ServiceExport declares that the Service with the same name and namespace // as this export should be consumable from other clusters. type ServiceExport struct { metav1.TypeMeta `json:",inline"` // +optional metav1.ObjectMeta `json:"metadata,omitempty"` // spec defines the behavior of a ServiceExport. // +optional Spec ServiceExportSpec `json:"spec,omitempty"` // status describes the current state of an exported service. // Service configuration comes from the Service that had the same // name and namespace as this ServiceExport. // Populated by the multi-cluster service implementation's controller. // +optional Status ServiceExportStatus `json:"status,omitempty"` } // ServiceExportSpec describes an exported service extra information type ServiceExportSpec struct { // exportedLabels describes the labels exported. It is optional for implementation. // +optional ExportedLabels map[string]string `json:"exportedLabels,omitempty"` // exportedAnnotations describes the annotations exported. It is optional for implementation. // +optional ExportedAnnotations map[string]string `json:"exportedAnnotations,omitempty"` } // ServiceExportStatus contains the current status of an export. type ServiceExportStatus struct { // +optional // +patchStrategy=merge // +patchMergeKey=type // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // +kubebuilder:object:root=true // ServiceExportList represents a list of endpoint slices type ServiceExportList struct { metav1.TypeMeta `json:",inline"` // Standard list metadata. // +optional metav1.ListMeta `json:"metadata,omitempty"` // List of endpoint slices // +listType=set Items []ServiceExport `json:"items"` } // ServiceExportConditionType is a type of condition associated with a // ServiceExport. This type should be used with the ServiceExportStatus.Conditions // field. type ServiceExportConditionType string // ServiceExportConditionReason defines the set of reasons that explain why a // particular ServiceExport condition type has been raised. type ServiceExportConditionReason string // NewServiceExportCondition creates a new ServiceExport condition func NewServiceExportCondition(t ServiceExportConditionType, status metav1.ConditionStatus, reason ServiceExportConditionReason, msg string) metav1.Condition { return metav1.Condition{ Type: string(t), Status: status, Reason: string(reason), Message: msg, LastTransitionTime: metav1.Now(), } } const ( // ServiceExportConditionValid is true when the Service Export is valid. // This does not indicate whether or not the configuration has been exported // to a control plane / data plane. // // // Possible reasons for this condition to be true are: // // * "Valid" // // Possible reasons for this condition to be False are: // // * "NoService" // * "InvalidServiceType" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. ServiceExportConditionValid ServiceExportConditionType = "Valid" // ServiceExportReasonValid is used with the "Valid" condition when the // condition is True. ServiceExportReasonValid ServiceExportConditionReason = "Valid" // ServiceExportReasonNoService is used with the "Valid" condition when // the associated Service does not exist. ServiceExportReasonNoService ServiceExportConditionReason = "NoService" // ServiceExportReasonInvalidServiceType is used with the "Valid" // condition when the associated Service has an invalid type // (per the KEP at least the ExternalName type). ServiceExportReasonInvalidServiceType ServiceExportConditionReason = "InvalidServiceType" ) const ( // ServiceExportConditionReady is true when the service is exported // to some control plane or data plane or ready to be pulled. // // // Possible reasons for this condition to be true are: // // * "Exported" // * "Ready" // // Possible reasons for this condition to be False are: // // * "Pending" // * "Failed" // // Possible reasons for this condition to be Unknown are: // // * "Pending" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. ServiceExportConditionReady ServiceExportConditionType = "Ready" // ServiceExportReasonExported is used with the "Ready" condition // when the condition is True and the service has been exported. // This would be used when an implementation exports a service // to a control plane or data plane. ServiceExportReasonExported ServiceExportConditionReason = "Exported" // ServiceExportReasonReady is used with the "Ready" condition // when the condition is True and the service has been exported. // This would typically be used in an implementation that uses a // pull model. ServiceExportReasonReady ServiceExportConditionReason = "Ready" // ServiceExportReasonPending is used with the "Ready" condition // when the service is in the process of being exported. ServiceExportReasonPending ServiceExportConditionReason = "Pending" // ServiceExportReasonFailed is used with the "Ready" condition // when the service failed to be exported with the message providing // the specific reason. ServiceExportReasonFailed ServiceExportConditionReason = "Failed" ) const ( // ServiceExportConditionConflict indicates that some property of an // exported service has conflicting values across the constituent // ServiceExports. This condition must be at least raised on the // conflicting ServiceExport and is recommended to be raised on all on // all the constituent ServiceExports if feasible. // // // Possible reasons for this condition to be true are: // // * "PortConflict" // * "TypeConflict" // * "SessionAffinityConflict" // * "SessionAffinityConfigConflict" // * "AnnotationsConflict" // * "LabelsConflict" // // When multiple conflicts occurs the above reasons may be combined // using commas. // // Possible reasons for this condition to be False are: // // * "NoConflicts" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. ServiceExportConditionConflict ServiceExportConditionType = "Conflict" // ServiceExportReasonPortConflict is used with the "Conflict" condition // when the exported service has a conflict related to port configuration // if the ports are not identical in all the constituent Services. ServiceExportReasonPortConflict ServiceExportConditionReason = "PortConflict" // ServiceExportReasonTypeConflict is used with the "Conflict" condition // when the exported service has a conflict related to the service type // (eg headless vs non-headless). ServiceExportReasonTypeConflict ServiceExportConditionReason = "TypeConflict" // ServiceExportReasonSessionAffinityConflict is used with the "Conflict" // condition when the exported service has a conflict related to session affinity. ServiceExportReasonSessionAffinityConflict ServiceExportConditionReason = "SessionAffinityConflict" // ServiceExportReasonSessionAffinityConfigConflict is used with the // "Conflict" condition when the exported service has a conflict related // to session affinity config. ServiceExportReasonSessionAffinityConfigConflict ServiceExportConditionReason = "SessionAffinityConfigConflict" // ServiceExportReasonLabelsConflict is used with the "Conflict" // condition when the ServiceExport has a conflict related to exported // labels. ServiceExportReasonLabelsConflict ServiceExportConditionReason = "LabelsConflict" // ServiceExportReasonAnnotationsConflict is used with the "Conflict" // condition when the ServiceExport has a conflict related to exported // annotations. ServiceExportReasonAnnotationsConflict ServiceExportConditionReason = "AnnotationsConflict" // ServiceExportReasonInternalTrafficPolicyConflict is used with the "Conflict" // condition when the exported service has a conflict related to internal traffic policy. ServiceExportReasonInternalTrafficPolicyConflict ServiceExportConditionReason = "InternalTrafficPolicyConflict" // ServiceExportReasonTrafficDistributionConflict is used with the "Conflict" // condition when the exported service has a conflict related to traffic distribution. ServiceExportReasonTrafficDistributionConflict ServiceExportConditionReason = "TrafficDistributionConflict" // ServiceExportReasonIPFamilyConflict is used with the "Conflict" condition // when the exported service has a conflict related to IPFamilies. // The handling of IP families is implementation-specific but this condition // must be used if a conflicting IP family may result in network traffic reaching // only a subset of the backends depending on the IP protocol used. ServiceExportReasonIPFamilyConflict ServiceExportConditionReason = "IPFamilyConflict" // ServiceExportReasonNoConflicts is used with the "Conflict" condition // when the condition is False. ServiceExportReasonNoConflicts ServiceExportConditionReason = "NoConflicts" ) ================================================ FILE: pkg/apis/v1beta1/serviceimport.go ================================================ /* Copyright 2025 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 v1beta1 import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( // ServiceImportPluralName is the plural name of ServiceImport ServiceImportPluralName = "serviceimports" // ServiceImportKindName is the kind name of ServiceImport ServiceImportKindName = "ServiceImport" // ServiceImportFullName is the full name of ServiceImport ServiceImportFullName = ServiceImportPluralName + "." + GroupName ) // ServiceImportVersionedName is the versioned name of ServiceImport var ServiceImportVersionedName = ServiceImportKindName + "/" + GroupVersion.Version // +genclient // +kubebuilder:object:root=true // +kubebuilder:resource:shortName={svcim,svcimport} // +kubebuilder:storageversion // ServiceImport describes a service imported from clusters in a ClusterSet. type ServiceImport struct { metav1.TypeMeta `json:",inline"` // +optional metav1.ObjectMeta `json:"metadata,omitempty"` // spec defines the behavior of a ServiceImport. // +optional Spec ServiceImportSpec `json:"spec,omitempty"` // status contains information about the exported services that form // the multi-cluster service referenced by this ServiceImport. // +optional Status ServiceImportStatus `json:"status,omitempty"` } // ServiceImportType designates the type of a ServiceImport type ServiceImportType string const ( // ClusterSetIP are only accessible via the ClusterSet IP. ClusterSetIP ServiceImportType = "ClusterSetIP" // Headless services allow backend pods to be addressed directly. Headless ServiceImportType = "Headless" ) // ServiceImportSpec describes an imported service and the information necessary to consume it. type ServiceImportSpec struct { // +listType=atomic Ports []ServicePort `json:"ports"` // ip will be used as the VIP for this service when type is ClusterSetIP. // +kubebuilder:validation:MaxItems:=2 // +optional IPs []string `json:"ips,omitempty"` // type defines the type of this service. // Must be ClusterSetIP or Headless. // +kubebuilder:validation:Enum=ClusterSetIP;Headless Type ServiceImportType `json:"type"` // Supports "ClientIP" and "None". Used to maintain session affinity. // Enable client IP based session affinity. // Must be ClientIP or None. // Defaults to None. // Ignored when type is Headless // More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies // +optional SessionAffinity v1.ServiceAffinity `json:"sessionAffinity,omitempty"` // sessionAffinityConfig contains session affinity configuration. // +optional SessionAffinityConfig *v1.SessionAffinityConfig `json:"sessionAffinityConfig,omitempty"` // IPFamilies identifies all the IPFamilies assigned for this ServiceImport. // +kubebuilder:validation:MaxItems:=2 // +optional IPFamilies []v1.IPFamily `json:"ipFamilies,omitempty"` // InternalTrafficPolicy describes how nodes distribute service traffic they // receive on the ClusterIP. If set to "Local", the proxy will assume that pods // only want to talk to endpoints of the service on the same node as the pod, // dropping the traffic if there are no local endpoints. The default value, // "Cluster", uses the standard behavior of routing to all endpoints evenly // (possibly modified by topology and other features). // +optional InternalTrafficPolicy *v1.ServiceInternalTrafficPolicy `json:"internalTrafficPolicy,omitempty"` // TrafficDistribution offers a way to express preferences for how traffic // is distributed to Service endpoints. Implementations can use this field // as a hint, but are not required to guarantee strict adherence. If the // field is not set, the implementation will apply its default routing // strategy. If set to "PreferClose", implementations should prioritize // endpoints that are in the same zone. // +optional TrafficDistribution *string `json:"trafficDistribution,omitempty"` } // ServicePort represents the port on which the service is exposed type ServicePort struct { // The name of this port within the service. This must be a DNS_LABEL. // All ports within a ServiceSpec must have unique names. When considering // the endpoints for a Service, this must match the 'name' field in the // EndpointPort. // Optional if only one ServicePort is defined on this service. // +optional Name string `json:"name,omitempty"` // The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". // Default is TCP. // +optional Protocol v1.Protocol `json:"protocol,omitempty"` // The application protocol for this port. // This is used as a hint for implementations to offer richer behavior for protocols that they understand. // This field follows standard Kubernetes label syntax. // Valid values are either: // // * Un-prefixed protocol names - reserved for IANA standard service names (as per // RFC-6335 and https://www.iana.org/assignments/service-names). // // * Kubernetes-defined prefixed names: // * 'kubernetes.io/h2c' - HTTP/2 over cleartext as described in https://www.rfc-editor.org/rfc/rfc7540 // // * Other protocols should use implementation-defined prefixed names such as // mycompany.com/my-custom-protocol. // Field can be enabled with ServiceAppProtocol feature gate. // +optional AppProtocol *string `json:"appProtocol,omitempty"` // The port that will be exposed by this service. Port int32 `json:"port"` } // ServiceImportStatus describes derived state of an imported service. type ServiceImportStatus struct { // clusters is the list of exporting clusters from which this service // was derived. // +optional // +patchStrategy=merge // +patchMergeKey=cluster // +listType=map // +listMapKey=cluster Clusters []ClusterStatus `json:"clusters,omitempty"` // +optional // +patchStrategy=merge // +patchMergeKey=type // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // ClusterStatus contains service configuration mapped to a specific source cluster type ClusterStatus struct { // cluster is the name of the exporting cluster. Must be a valid RFC-1123 DNS // label. Cluster string `json:"cluster"` } // +kubebuilder:object:root=true // ServiceImportList represents a list of endpoint slices type ServiceImportList struct { metav1.TypeMeta `json:",inline"` // Standard list metadata. // +optional metav1.ListMeta `json:"metadata,omitempty"` // List of endpoint slices // +listType=set Items []ServiceImport `json:"items"` } // ServiceImportConditionType is a type of condition associated with a // ServiceImport. This type should be used with the ServiceImportStatus.Conditions // field. type ServiceImportConditionType string // ServiceImportConditionReason defines the set of reasons that explain why a // particular ServiceImport condition type has been raised. type ServiceImportConditionReason string // NewServiceImportCondition creates a new ServiceImport condition func NewServiceImportCondition(t ServiceImportConditionType, status metav1.ConditionStatus, reason ServiceImportConditionReason, msg string) metav1.Condition { return metav1.Condition{ Type: string(t), Status: status, Reason: string(reason), Message: msg, LastTransitionTime: metav1.Now(), } } const ( // ServiceImportConditionReady is true when the Service Import is ready. // // // Possible reasons for this condition to be true are: // // * "Ready" // // Possible reasons for this condition to be False are: // // * "Pending" // * "IPFamilyNotSupported" // // Possible reasons for this condition to be Unknown are: // // * "Pending" // // Controllers may raise this condition with other reasons, // but should prefer to use the reasons listed above to improve // interoperability. ServiceImportConditionReady ServiceImportConditionType = "Ready" // ServiceImportReasonReady is used with the "Ready" condition when the // condition is True. ServiceImportReasonReady ServiceImportConditionReason = "Ready" // ServiceImportReasonPending is used with the "Ready" condition when // the ServiceImport is in the process of being created or updated. ServiceImportReasonPending ServiceImportConditionReason = "Pending" // ServiceImportReasonIPFamilyNotSupported is used with the "Ready" // condition when the service can not be imported due to IP families // mismatch. ServiceImportReasonIPFamilyNotSupported ServiceImportConditionReason = "IPFamilyNotSupported" ) ================================================ FILE: pkg/apis/v1beta1/well_known_labels.go ================================================ /* Copyright 2025 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 v1beta1 const ( // LabelServiceName is used to indicate the name of multi-cluster service // that an EndpointSlice belongs to. LabelServiceName = "multicluster.kubernetes.io/service-name" // LabelSourceCluster is used to indicate the name of the cluster in which an exported resource exists. LabelSourceCluster = "multicluster.kubernetes.io/source-cluster" ) ================================================ FILE: pkg/apis/v1beta1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2020 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. */ // Code generated by controller-gen. DO NOT EDIT. package v1beta1 import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = *in } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. func (in *ClusterStatus) DeepCopy() *ClusterStatus { if in == nil { return nil } out := new(ClusterStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExport) DeepCopyInto(out *ServiceExport) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExport. func (in *ServiceExport) DeepCopy() *ServiceExport { if in == nil { return nil } out := new(ServiceExport) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ServiceExport) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExportList) DeepCopyInto(out *ServiceExportList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]ServiceExport, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExportList. func (in *ServiceExportList) DeepCopy() *ServiceExportList { if in == nil { return nil } out := new(ServiceExportList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ServiceExportList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExportSpec) DeepCopyInto(out *ServiceExportSpec) { *out = *in if in.ExportedLabels != nil { in, out := &in.ExportedLabels, &out.ExportedLabels *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.ExportedAnnotations != nil { in, out := &in.ExportedAnnotations, &out.ExportedAnnotations *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExportSpec. func (in *ServiceExportSpec) DeepCopy() *ServiceExportSpec { if in == nil { return nil } out := new(ServiceExportSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExportStatus) DeepCopyInto(out *ServiceExportStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExportStatus. func (in *ServiceExportStatus) DeepCopy() *ServiceExportStatus { if in == nil { return nil } out := new(ServiceExportStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceImport) DeepCopyInto(out *ServiceImport) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImport. func (in *ServiceImport) DeepCopy() *ServiceImport { if in == nil { return nil } out := new(ServiceImport) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ServiceImport) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceImportList) DeepCopyInto(out *ServiceImportList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]ServiceImport, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImportList. func (in *ServiceImportList) DeepCopy() *ServiceImportList { if in == nil { return nil } out := new(ServiceImportList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *ServiceImportList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceImportSpec) DeepCopyInto(out *ServiceImportSpec) { *out = *in if in.Ports != nil { in, out := &in.Ports, &out.Ports *out = make([]ServicePort, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.IPs != nil { in, out := &in.IPs, &out.IPs *out = make([]string, len(*in)) copy(*out, *in) } if in.SessionAffinityConfig != nil { in, out := &in.SessionAffinityConfig, &out.SessionAffinityConfig *out = new(corev1.SessionAffinityConfig) (*in).DeepCopyInto(*out) } if in.IPFamilies != nil { in, out := &in.IPFamilies, &out.IPFamilies *out = make([]corev1.IPFamily, len(*in)) copy(*out, *in) } if in.InternalTrafficPolicy != nil { in, out := &in.InternalTrafficPolicy, &out.InternalTrafficPolicy *out = new(corev1.ServiceInternalTrafficPolicy) **out = **in } if in.TrafficDistribution != nil { in, out := &in.TrafficDistribution, &out.TrafficDistribution *out = new(string) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImportSpec. func (in *ServiceImportSpec) DeepCopy() *ServiceImportSpec { if in == nil { return nil } out := new(ServiceImportSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceImportStatus) DeepCopyInto(out *ServiceImportStatus) { *out = *in if in.Clusters != nil { in, out := &in.Clusters, &out.Clusters *out = make([]ClusterStatus, len(*in)) copy(*out, *in) } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceImportStatus. func (in *ServiceImportStatus) DeepCopy() *ServiceImportStatus { if in == nil { return nil } out := new(ServiceImportStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServicePort) DeepCopyInto(out *ServicePort) { *out = *in if in.AppProtocol != nil { in, out := &in.AppProtocol, &out.AppProtocol *out = new(string) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServicePort. func (in *ServicePort) DeepCopy() *ServicePort { if in == nil { return nil } out := new(ServicePort) in.DeepCopyInto(out) return out } ================================================ FILE: pkg/apis/v1beta1/zz_generated.register.go ================================================ //go:build !ignore_autogenerated // +build !ignore_autogenerated /* Copyright 2020 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. */ // Code generated by register-gen. DO NOT EDIT. package v1beta1 import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" ) // GroupName specifies the group name used to register the objects. const GroupName = "multicluster.x-k8s.io" // GroupVersion specifies the group and the version used to register the objects. var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1beta1"} // SchemeGroupVersion is group version used to register these objects // Deprecated: use GroupVersion instead. var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"} // Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder // Deprecated: use Install instead AddToScheme = localSchemeBuilder.AddToScheme Install = localSchemeBuilder.AddToScheme ) func init() { // We only register manually written functions here. The registration of the // generated functions takes place in the generated files. The separation // makes the code compile even when the generated files are missing. localSchemeBuilder.Register(addKnownTypes) } // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &ServiceExport{}, &ServiceExportList{}, &ServiceImport{}, &ServiceImportList{}, ) // AddToGroupVersion allows the serialization of client types like ListOptions. v1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ================================================ FILE: pkg/client/clientset/versioned/clientset.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package versioned import ( fmt "fmt" http "net/http" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" multiclusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" multiclusterv1beta1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1beta1" ) type Interface interface { Discovery() discovery.DiscoveryInterface MulticlusterV1alpha1() multiclusterv1alpha1.MulticlusterV1alpha1Interface MulticlusterV1beta1() multiclusterv1beta1.MulticlusterV1beta1Interface } // Clientset contains the clients for groups. type Clientset struct { *discovery.DiscoveryClient multiclusterV1alpha1 *multiclusterv1alpha1.MulticlusterV1alpha1Client multiclusterV1beta1 *multiclusterv1beta1.MulticlusterV1beta1Client } // MulticlusterV1alpha1 retrieves the MulticlusterV1alpha1Client func (c *Clientset) MulticlusterV1alpha1() multiclusterv1alpha1.MulticlusterV1alpha1Interface { return c.multiclusterV1alpha1 } // MulticlusterV1beta1 retrieves the MulticlusterV1beta1Client func (c *Clientset) MulticlusterV1beta1() multiclusterv1beta1.MulticlusterV1beta1Interface { return c.multiclusterV1beta1 } // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { return nil } return c.DiscoveryClient } // NewForConfig creates a new Clientset for the given config. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfig will generate a rate-limiter in configShallowCopy. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.UserAgent == "" { configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() } // share the transport between all clients httpClient, err := rest.HTTPClientFor(&configShallowCopy) if err != nil { return nil, err } return NewForConfigAndClient(&configShallowCopy, httpClient) } // NewForConfigAndClient creates a new Clientset for the given config and http client. // Note the http client provided takes precedence over the configured transport values. // If config's RateLimiter is not set and QPS and Burst are acceptable, // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { if configShallowCopy.Burst <= 0 { return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") } configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) } var cs Clientset var err error cs.multiclusterV1alpha1, err = multiclusterv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } cs.multiclusterV1beta1, err = multiclusterv1beta1.NewForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) if err != nil { return nil, err } return &cs, nil } // NewForConfigOrDie creates a new Clientset for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *Clientset { cs, err := NewForConfig(c) if err != nil { panic(err) } return cs } // New creates a new Clientset for the given RESTClient. func New(c rest.Interface) *Clientset { var cs Clientset cs.multiclusterV1alpha1 = multiclusterv1alpha1.New(c) cs.multiclusterV1beta1 = multiclusterv1beta1.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) return &cs } ================================================ FILE: pkg/client/clientset/versioned/fake/clientset_generated.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" fakediscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/testing" clientset "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" multiclusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" fakemulticlusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake" multiclusterv1beta1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1beta1" fakemulticlusterv1beta1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1beta1/fake" ) // NewSimpleClientset returns a clientset that will respond with the provided objects. // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, // without applying any field management, validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. // // DEPRECATED: NewClientset replaces this with support for field management, which significantly improves // server side apply testing. NewClientset is only available when apply configurations are generated (e.g. // via --with-applyconfig). func NewSimpleClientset(objects ...runtime.Object) *Clientset { o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) } } cs := &Clientset{tracker: o} cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { gvr := action.GetResource() ns := action.GetNamespace() watch, err := o.Watch(gvr, ns) if err != nil { return false, nil, err } return true, watch, nil }) return cs } // Clientset implements clientset.Interface. Meant to be embedded into a // struct to get a default implementation. This makes faking out just the method // you want to test easier. type Clientset struct { testing.Fake discovery *fakediscovery.FakeDiscovery tracker testing.ObjectTracker } func (c *Clientset) Discovery() discovery.DiscoveryInterface { return c.discovery } func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } var ( _ clientset.Interface = &Clientset{} _ testing.FakeClient = &Clientset{} ) // MulticlusterV1alpha1 retrieves the MulticlusterV1alpha1Client func (c *Clientset) MulticlusterV1alpha1() multiclusterv1alpha1.MulticlusterV1alpha1Interface { return &fakemulticlusterv1alpha1.FakeMulticlusterV1alpha1{Fake: &c.Fake} } // MulticlusterV1beta1 retrieves the MulticlusterV1beta1Client func (c *Clientset) MulticlusterV1beta1() multiclusterv1beta1.MulticlusterV1beta1Interface { return &fakemulticlusterv1beta1.FakeMulticlusterV1beta1{Fake: &c.Fake} } ================================================ FILE: pkg/client/clientset/versioned/fake/doc.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated fake clientset. package fake ================================================ FILE: pkg/client/clientset/versioned/fake/register.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" multiclusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" multiclusterv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var localSchemeBuilder = runtime.SchemeBuilder{ multiclusterv1alpha1.AddToScheme, multiclusterv1beta1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(scheme)) } ================================================ FILE: pkg/client/clientset/versioned/scheme/doc.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. // This package contains the scheme of the automatically generated clientset. package scheme ================================================ FILE: pkg/client/clientset/versioned/scheme/register.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package scheme import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" multiclusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" multiclusterv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) var localSchemeBuilder = runtime.SchemeBuilder{ multiclusterv1alpha1.AddToScheme, multiclusterv1beta1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition // of clientsets, like in: // // import ( // "k8s.io/client-go/kubernetes" // clientsetscheme "k8s.io/client-go/kubernetes/scheme" // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" // ) // // kclientset, _ := kubernetes.NewForConfig(c) // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. var AddToScheme = localSchemeBuilder.AddToScheme func init() { v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(Scheme)) } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package v1alpha1 import ( http "net/http" rest "k8s.io/client-go/rest" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" ) type MulticlusterV1alpha1Interface interface { RESTClient() rest.Interface ServiceExportsGetter ServiceImportsGetter } // MulticlusterV1alpha1Client is used to interact with features provided by the multicluster.x-k8s.io group. type MulticlusterV1alpha1Client struct { restClient rest.Interface } func (c *MulticlusterV1alpha1Client) ServiceExports(namespace string) ServiceExportInterface { return newServiceExports(c, namespace) } func (c *MulticlusterV1alpha1Client) ServiceImports(namespace string) ServiceImportInterface { return newServiceImports(c, namespace) } // NewForConfig creates a new MulticlusterV1alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*MulticlusterV1alpha1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } httpClient, err := rest.HTTPClientFor(&config) if err != nil { return nil, err } return NewForConfigAndClient(&config, httpClient) } // NewForConfigAndClient creates a new MulticlusterV1alpha1Client for the given config and http client. // Note the http client provided takes precedence over the configured transport values. func NewForConfigAndClient(c *rest.Config, h *http.Client) (*MulticlusterV1alpha1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err } return &MulticlusterV1alpha1Client{client}, nil } // NewForConfigOrDie creates a new MulticlusterV1alpha1Client for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *MulticlusterV1alpha1Client { client, err := NewForConfig(c) if err != nil { panic(err) } return client } // New creates a new MulticlusterV1alpha1Client for the given RESTClient. func New(c rest.Interface) *MulticlusterV1alpha1Client { return &MulticlusterV1alpha1Client{c} } func setConfigDefaults(config *rest.Config) error { gv := apisv1alpha1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } return nil } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *MulticlusterV1alpha1Client) RESTClient() rest.Interface { if c == nil { return nil } return c.restClient } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/doc.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated typed clients. package v1alpha1 ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. // Package fake has the automatically generated clients. package fake ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" v1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" ) type FakeMulticlusterV1alpha1 struct { *testing.Fake } func (c *FakeMulticlusterV1alpha1) ServiceExports(namespace string) v1alpha1.ServiceExportInterface { return newFakeServiceExports(c, namespace) } func (c *FakeMulticlusterV1alpha1) ServiceImports(namespace string) v1alpha1.ServiceImportInterface { return newFakeServiceImports(c, namespace) } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeMulticlusterV1alpha1) RESTClient() rest.Interface { var ret *rest.RESTClient return ret } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_serviceexport.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( gentype "k8s.io/client-go/gentype" v1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" ) // fakeServiceExports implements ServiceExportInterface type fakeServiceExports struct { *gentype.FakeClientWithList[*v1alpha1.ServiceExport, *v1alpha1.ServiceExportList] Fake *FakeMulticlusterV1alpha1 } func newFakeServiceExports(fake *FakeMulticlusterV1alpha1, namespace string) apisv1alpha1.ServiceExportInterface { return &fakeServiceExports{ gentype.NewFakeClientWithList[*v1alpha1.ServiceExport, *v1alpha1.ServiceExportList]( fake.Fake, namespace, v1alpha1.SchemeGroupVersion.WithResource("serviceexports"), v1alpha1.SchemeGroupVersion.WithKind("ServiceExport"), func() *v1alpha1.ServiceExport { return &v1alpha1.ServiceExport{} }, func() *v1alpha1.ServiceExportList { return &v1alpha1.ServiceExportList{} }, func(dst, src *v1alpha1.ServiceExportList) { dst.ListMeta = src.ListMeta }, func(list *v1alpha1.ServiceExportList) []*v1alpha1.ServiceExport { return gentype.ToPointerSlice(list.Items) }, func(list *v1alpha1.ServiceExportList, items []*v1alpha1.ServiceExport) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_serviceimport.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( gentype "k8s.io/client-go/gentype" v1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" ) // fakeServiceImports implements ServiceImportInterface type fakeServiceImports struct { *gentype.FakeClientWithList[*v1alpha1.ServiceImport, *v1alpha1.ServiceImportList] Fake *FakeMulticlusterV1alpha1 } func newFakeServiceImports(fake *FakeMulticlusterV1alpha1, namespace string) apisv1alpha1.ServiceImportInterface { return &fakeServiceImports{ gentype.NewFakeClientWithList[*v1alpha1.ServiceImport, *v1alpha1.ServiceImportList]( fake.Fake, namespace, v1alpha1.SchemeGroupVersion.WithResource("serviceimports"), v1alpha1.SchemeGroupVersion.WithKind("ServiceImport"), func() *v1alpha1.ServiceImport { return &v1alpha1.ServiceImport{} }, func() *v1alpha1.ServiceImportList { return &v1alpha1.ServiceImportList{} }, func(dst, src *v1alpha1.ServiceImportList) { dst.ListMeta = src.ListMeta }, func(list *v1alpha1.ServiceImportList) []*v1alpha1.ServiceImport { return gentype.ToPointerSlice(list.Items) }, func(list *v1alpha1.ServiceImportList, items []*v1alpha1.ServiceImport) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package v1alpha1 type ServiceExportExpansion interface{} type ServiceImportExpansion interface{} ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/serviceexport.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package v1alpha1 import ( context "context" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" ) // ServiceExportsGetter has a method to return a ServiceExportInterface. // A group's client should implement this interface. type ServiceExportsGetter interface { ServiceExports(namespace string) ServiceExportInterface } // ServiceExportInterface has methods to work with ServiceExport resources. type ServiceExportInterface interface { Create(ctx context.Context, serviceExport *apisv1alpha1.ServiceExport, opts v1.CreateOptions) (*apisv1alpha1.ServiceExport, error) Update(ctx context.Context, serviceExport *apisv1alpha1.ServiceExport, opts v1.UpdateOptions) (*apisv1alpha1.ServiceExport, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, serviceExport *apisv1alpha1.ServiceExport, opts v1.UpdateOptions) (*apisv1alpha1.ServiceExport, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*apisv1alpha1.ServiceExport, error) List(ctx context.Context, opts v1.ListOptions) (*apisv1alpha1.ServiceExportList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apisv1alpha1.ServiceExport, err error) ServiceExportExpansion } // serviceExports implements ServiceExportInterface type serviceExports struct { *gentype.ClientWithList[*apisv1alpha1.ServiceExport, *apisv1alpha1.ServiceExportList] } // newServiceExports returns a ServiceExports func newServiceExports(c *MulticlusterV1alpha1Client, namespace string) *serviceExports { return &serviceExports{ gentype.NewClientWithList[*apisv1alpha1.ServiceExport, *apisv1alpha1.ServiceExportList]( "serviceexports", c.RESTClient(), scheme.ParameterCodec, namespace, func() *apisv1alpha1.ServiceExport { return &apisv1alpha1.ServiceExport{} }, func() *apisv1alpha1.ServiceExportList { return &apisv1alpha1.ServiceExportList{} }, ), } } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1alpha1/serviceimport.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package v1alpha1 import ( context "context" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" ) // ServiceImportsGetter has a method to return a ServiceImportInterface. // A group's client should implement this interface. type ServiceImportsGetter interface { ServiceImports(namespace string) ServiceImportInterface } // ServiceImportInterface has methods to work with ServiceImport resources. type ServiceImportInterface interface { Create(ctx context.Context, serviceImport *apisv1alpha1.ServiceImport, opts v1.CreateOptions) (*apisv1alpha1.ServiceImport, error) Update(ctx context.Context, serviceImport *apisv1alpha1.ServiceImport, opts v1.UpdateOptions) (*apisv1alpha1.ServiceImport, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, serviceImport *apisv1alpha1.ServiceImport, opts v1.UpdateOptions) (*apisv1alpha1.ServiceImport, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*apisv1alpha1.ServiceImport, error) List(ctx context.Context, opts v1.ListOptions) (*apisv1alpha1.ServiceImportList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apisv1alpha1.ServiceImport, err error) ServiceImportExpansion } // serviceImports implements ServiceImportInterface type serviceImports struct { *gentype.ClientWithList[*apisv1alpha1.ServiceImport, *apisv1alpha1.ServiceImportList] } // newServiceImports returns a ServiceImports func newServiceImports(c *MulticlusterV1alpha1Client, namespace string) *serviceImports { return &serviceImports{ gentype.NewClientWithList[*apisv1alpha1.ServiceImport, *apisv1alpha1.ServiceImportList]( "serviceimports", c.RESTClient(), scheme.ParameterCodec, namespace, func() *apisv1alpha1.ServiceImport { return &apisv1alpha1.ServiceImport{} }, func() *apisv1alpha1.ServiceImportList { return &apisv1alpha1.ServiceImportList{} }, ), } } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/apis_client.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package v1beta1 import ( http "net/http" rest "k8s.io/client-go/rest" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" ) type MulticlusterV1beta1Interface interface { RESTClient() rest.Interface ServiceExportsGetter ServiceImportsGetter } // MulticlusterV1beta1Client is used to interact with features provided by the multicluster.x-k8s.io group. type MulticlusterV1beta1Client struct { restClient rest.Interface } func (c *MulticlusterV1beta1Client) ServiceExports(namespace string) ServiceExportInterface { return newServiceExports(c, namespace) } func (c *MulticlusterV1beta1Client) ServiceImports(namespace string) ServiceImportInterface { return newServiceImports(c, namespace) } // NewForConfig creates a new MulticlusterV1beta1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*MulticlusterV1beta1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } httpClient, err := rest.HTTPClientFor(&config) if err != nil { return nil, err } return NewForConfigAndClient(&config, httpClient) } // NewForConfigAndClient creates a new MulticlusterV1beta1Client for the given config and http client. // Note the http client provided takes precedence over the configured transport values. func NewForConfigAndClient(c *rest.Config, h *http.Client) (*MulticlusterV1beta1Client, error) { config := *c if err := setConfigDefaults(&config); err != nil { return nil, err } client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err } return &MulticlusterV1beta1Client{client}, nil } // NewForConfigOrDie creates a new MulticlusterV1beta1Client for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *MulticlusterV1beta1Client { client, err := NewForConfig(c) if err != nil { panic(err) } return client } // New creates a new MulticlusterV1beta1Client for the given RESTClient. func New(c rest.Interface) *MulticlusterV1beta1Client { return &MulticlusterV1beta1Client{c} } func setConfigDefaults(config *rest.Config) error { gv := apisv1beta1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } return nil } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *MulticlusterV1beta1Client) RESTClient() rest.Interface { if c == nil { return nil } return c.restClient } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/doc.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. // This package has the automatically generated typed clients. package v1beta1 ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/fake/doc.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. // Package fake has the automatically generated clients. package fake ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/fake/fake_apis_client.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" v1beta1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1beta1" ) type FakeMulticlusterV1beta1 struct { *testing.Fake } func (c *FakeMulticlusterV1beta1) ServiceExports(namespace string) v1beta1.ServiceExportInterface { return newFakeServiceExports(c, namespace) } func (c *FakeMulticlusterV1beta1) ServiceImports(namespace string) v1beta1.ServiceImportInterface { return newFakeServiceImports(c, namespace) } // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeMulticlusterV1beta1) RESTClient() rest.Interface { var ret *rest.RESTClient return ret } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/fake/fake_serviceexport.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( gentype "k8s.io/client-go/gentype" v1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1beta1" ) // fakeServiceExports implements ServiceExportInterface type fakeServiceExports struct { *gentype.FakeClientWithList[*v1beta1.ServiceExport, *v1beta1.ServiceExportList] Fake *FakeMulticlusterV1beta1 } func newFakeServiceExports(fake *FakeMulticlusterV1beta1, namespace string) apisv1beta1.ServiceExportInterface { return &fakeServiceExports{ gentype.NewFakeClientWithList[*v1beta1.ServiceExport, *v1beta1.ServiceExportList]( fake.Fake, namespace, v1beta1.SchemeGroupVersion.WithResource("serviceexports"), v1beta1.SchemeGroupVersion.WithKind("ServiceExport"), func() *v1beta1.ServiceExport { return &v1beta1.ServiceExport{} }, func() *v1beta1.ServiceExportList { return &v1beta1.ServiceExportList{} }, func(dst, src *v1beta1.ServiceExportList) { dst.ListMeta = src.ListMeta }, func(list *v1beta1.ServiceExportList) []*v1beta1.ServiceExport { return gentype.ToPointerSlice(list.Items) }, func(list *v1beta1.ServiceExportList, items []*v1beta1.ServiceExport) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/fake/fake_serviceimport.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package fake import ( gentype "k8s.io/client-go/gentype" v1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1beta1" ) // fakeServiceImports implements ServiceImportInterface type fakeServiceImports struct { *gentype.FakeClientWithList[*v1beta1.ServiceImport, *v1beta1.ServiceImportList] Fake *FakeMulticlusterV1beta1 } func newFakeServiceImports(fake *FakeMulticlusterV1beta1, namespace string) apisv1beta1.ServiceImportInterface { return &fakeServiceImports{ gentype.NewFakeClientWithList[*v1beta1.ServiceImport, *v1beta1.ServiceImportList]( fake.Fake, namespace, v1beta1.SchemeGroupVersion.WithResource("serviceimports"), v1beta1.SchemeGroupVersion.WithKind("ServiceImport"), func() *v1beta1.ServiceImport { return &v1beta1.ServiceImport{} }, func() *v1beta1.ServiceImportList { return &v1beta1.ServiceImportList{} }, func(dst, src *v1beta1.ServiceImportList) { dst.ListMeta = src.ListMeta }, func(list *v1beta1.ServiceImportList) []*v1beta1.ServiceImport { return gentype.ToPointerSlice(list.Items) }, func(list *v1beta1.ServiceImportList, items []*v1beta1.ServiceImport) { list.Items = gentype.FromPointerSlice(items) }, ), fake, } } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/generated_expansion.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package v1beta1 type ServiceExportExpansion interface{} type ServiceImportExpansion interface{} ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/serviceexport.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package v1beta1 import ( context "context" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" ) // ServiceExportsGetter has a method to return a ServiceExportInterface. // A group's client should implement this interface. type ServiceExportsGetter interface { ServiceExports(namespace string) ServiceExportInterface } // ServiceExportInterface has methods to work with ServiceExport resources. type ServiceExportInterface interface { Create(ctx context.Context, serviceExport *apisv1beta1.ServiceExport, opts v1.CreateOptions) (*apisv1beta1.ServiceExport, error) Update(ctx context.Context, serviceExport *apisv1beta1.ServiceExport, opts v1.UpdateOptions) (*apisv1beta1.ServiceExport, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, serviceExport *apisv1beta1.ServiceExport, opts v1.UpdateOptions) (*apisv1beta1.ServiceExport, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*apisv1beta1.ServiceExport, error) List(ctx context.Context, opts v1.ListOptions) (*apisv1beta1.ServiceExportList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apisv1beta1.ServiceExport, err error) ServiceExportExpansion } // serviceExports implements ServiceExportInterface type serviceExports struct { *gentype.ClientWithList[*apisv1beta1.ServiceExport, *apisv1beta1.ServiceExportList] } // newServiceExports returns a ServiceExports func newServiceExports(c *MulticlusterV1beta1Client, namespace string) *serviceExports { return &serviceExports{ gentype.NewClientWithList[*apisv1beta1.ServiceExport, *apisv1beta1.ServiceExportList]( "serviceexports", c.RESTClient(), scheme.ParameterCodec, namespace, func() *apisv1beta1.ServiceExport { return &apisv1beta1.ServiceExport{} }, func() *apisv1beta1.ServiceExportList { return &apisv1beta1.ServiceExportList{} }, ), } } ================================================ FILE: pkg/client/clientset/versioned/typed/apis/v1beta1/serviceimport.go ================================================ /* Copyright 2020 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. */ // Code generated by client-gen. DO NOT EDIT. package v1beta1 import ( context "context" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" gentype "k8s.io/client-go/gentype" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" ) // ServiceImportsGetter has a method to return a ServiceImportInterface. // A group's client should implement this interface. type ServiceImportsGetter interface { ServiceImports(namespace string) ServiceImportInterface } // ServiceImportInterface has methods to work with ServiceImport resources. type ServiceImportInterface interface { Create(ctx context.Context, serviceImport *apisv1beta1.ServiceImport, opts v1.CreateOptions) (*apisv1beta1.ServiceImport, error) Update(ctx context.Context, serviceImport *apisv1beta1.ServiceImport, opts v1.UpdateOptions) (*apisv1beta1.ServiceImport, error) // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). UpdateStatus(ctx context.Context, serviceImport *apisv1beta1.ServiceImport, opts v1.UpdateOptions) (*apisv1beta1.ServiceImport, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*apisv1beta1.ServiceImport, error) List(ctx context.Context, opts v1.ListOptions) (*apisv1beta1.ServiceImportList, error) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apisv1beta1.ServiceImport, err error) ServiceImportExpansion } // serviceImports implements ServiceImportInterface type serviceImports struct { *gentype.ClientWithList[*apisv1beta1.ServiceImport, *apisv1beta1.ServiceImportList] } // newServiceImports returns a ServiceImports func newServiceImports(c *MulticlusterV1beta1Client, namespace string) *serviceImports { return &serviceImports{ gentype.NewClientWithList[*apisv1beta1.ServiceImport, *apisv1beta1.ServiceImportList]( "serviceimports", c.RESTClient(), scheme.ParameterCodec, namespace, func() *apisv1beta1.ServiceImport { return &apisv1beta1.ServiceImport{} }, func() *apisv1beta1.ServiceImportList { return &apisv1beta1.ServiceImportList{} }, ), } } ================================================ FILE: pkg/client/informers/externalversions/apis/interface.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package apis import ( v1alpha1 "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/apis/v1alpha1" v1beta1 "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/apis/v1beta1" internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to each of this group's versions. type Interface interface { // V1alpha1 provides access to shared informers for resources in V1alpha1. V1alpha1() v1alpha1.Interface // V1beta1 provides access to shared informers for resources in V1beta1. V1beta1() v1beta1.Interface } type group struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // V1alpha1 returns a new v1alpha1.Interface. func (g *group) V1alpha1() v1alpha1.Interface { return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) } // V1beta1 returns a new v1beta1.Interface. func (g *group) V1beta1() v1beta1.Interface { return v1beta1.New(g.factory, g.namespace, g.tweakListOptions) } ================================================ FILE: pkg/client/informers/externalversions/apis/v1alpha1/interface.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package v1alpha1 import ( internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to all the informers in this group version. type Interface interface { // ServiceExports returns a ServiceExportInformer. ServiceExports() ServiceExportInformer // ServiceImports returns a ServiceImportInformer. ServiceImports() ServiceImportInformer } type version struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // ServiceExports returns a ServiceExportInformer. func (v *version) ServiceExports() ServiceExportInformer { return &serviceExportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } // ServiceImports returns a ServiceImportInformer. func (v *version) ServiceImports() ServiceImportInformer { return &serviceImportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } ================================================ FILE: pkg/client/informers/externalversions/apis/v1alpha1/serviceexport.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package v1alpha1 import ( context "context" time "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" pkgapisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/listers/apis/v1alpha1" ) // ServiceExportInformer provides access to a shared informer and lister for // ServiceExports. type ServiceExportInformer interface { Informer() cache.SharedIndexInformer Lister() apisv1alpha1.ServiceExportLister } type serviceExportInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewServiceExportInformer constructs a new informer for ServiceExport type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewServiceExportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredServiceExportInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredServiceExportInformer constructs a new informer for ServiceExport type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredServiceExportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.MulticlusterV1alpha1().ServiceExports(namespace).List(context.TODO(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.MulticlusterV1alpha1().ServiceExports(namespace).Watch(context.TODO(), options) }, }, &pkgapisv1alpha1.ServiceExport{}, resyncPeriod, indexers, ) } func (f *serviceExportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredServiceExportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *serviceExportInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&pkgapisv1alpha1.ServiceExport{}, f.defaultInformer) } func (f *serviceExportInformer) Lister() apisv1alpha1.ServiceExportLister { return apisv1alpha1.NewServiceExportLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/client/informers/externalversions/apis/v1alpha1/serviceimport.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package v1alpha1 import ( context "context" time "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" pkgapisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/listers/apis/v1alpha1" ) // ServiceImportInformer provides access to a shared informer and lister for // ServiceImports. type ServiceImportInformer interface { Informer() cache.SharedIndexInformer Lister() apisv1alpha1.ServiceImportLister } type serviceImportInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewServiceImportInformer constructs a new informer for ServiceImport type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewServiceImportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredServiceImportInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredServiceImportInformer constructs a new informer for ServiceImport type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredServiceImportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.MulticlusterV1alpha1().ServiceImports(namespace).List(context.TODO(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.MulticlusterV1alpha1().ServiceImports(namespace).Watch(context.TODO(), options) }, }, &pkgapisv1alpha1.ServiceImport{}, resyncPeriod, indexers, ) } func (f *serviceImportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredServiceImportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *serviceImportInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&pkgapisv1alpha1.ServiceImport{}, f.defaultInformer) } func (f *serviceImportInformer) Lister() apisv1alpha1.ServiceImportLister { return apisv1alpha1.NewServiceImportLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/client/informers/externalversions/apis/v1beta1/interface.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package v1beta1 import ( internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to all the informers in this group version. type Interface interface { // ServiceExports returns a ServiceExportInformer. ServiceExports() ServiceExportInformer // ServiceImports returns a ServiceImportInformer. ServiceImports() ServiceImportInformer } type version struct { factory internalinterfaces.SharedInformerFactory namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc } // New returns a new Interface. func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } // ServiceExports returns a ServiceExportInformer. func (v *version) ServiceExports() ServiceExportInformer { return &serviceExportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } // ServiceImports returns a ServiceImportInformer. func (v *version) ServiceImports() ServiceImportInformer { return &serviceImportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } ================================================ FILE: pkg/client/informers/externalversions/apis/v1beta1/serviceexport.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package v1beta1 import ( context "context" time "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" pkgapisv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/client/listers/apis/v1beta1" ) // ServiceExportInformer provides access to a shared informer and lister for // ServiceExports. type ServiceExportInformer interface { Informer() cache.SharedIndexInformer Lister() apisv1beta1.ServiceExportLister } type serviceExportInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewServiceExportInformer constructs a new informer for ServiceExport type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewServiceExportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredServiceExportInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredServiceExportInformer constructs a new informer for ServiceExport type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredServiceExportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.MulticlusterV1beta1().ServiceExports(namespace).List(context.TODO(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.MulticlusterV1beta1().ServiceExports(namespace).Watch(context.TODO(), options) }, }, &pkgapisv1beta1.ServiceExport{}, resyncPeriod, indexers, ) } func (f *serviceExportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredServiceExportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *serviceExportInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&pkgapisv1beta1.ServiceExport{}, f.defaultInformer) } func (f *serviceExportInformer) Lister() apisv1beta1.ServiceExportLister { return apisv1beta1.NewServiceExportLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/client/informers/externalversions/apis/v1beta1/serviceimport.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package v1beta1 import ( context "context" time "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" pkgapisv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/client/listers/apis/v1beta1" ) // ServiceImportInformer provides access to a shared informer and lister for // ServiceImports. type ServiceImportInformer interface { Informer() cache.SharedIndexInformer Lister() apisv1beta1.ServiceImportLister } type serviceImportInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc namespace string } // NewServiceImportInformer constructs a new informer for ServiceImport type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewServiceImportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { return NewFilteredServiceImportInformer(client, namespace, resyncPeriod, indexers, nil) } // NewFilteredServiceImportInformer constructs a new informer for ServiceImport type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredServiceImportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.MulticlusterV1beta1().ServiceImports(namespace).List(context.TODO(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.MulticlusterV1beta1().ServiceImports(namespace).Watch(context.TODO(), options) }, }, &pkgapisv1beta1.ServiceImport{}, resyncPeriod, indexers, ) } func (f *serviceImportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredServiceImportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *serviceImportInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&pkgapisv1beta1.ServiceImport{}, f.defaultInformer) } func (f *serviceImportInformer) Lister() apisv1beta1.ServiceImportLister { return apisv1beta1.NewServiceImportLister(f.Informer().GetIndexer()) } ================================================ FILE: pkg/client/informers/externalversions/factory.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( reflect "reflect" sync "sync" time "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" apis "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/apis" internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" ) // SharedInformerOption defines the functional option type for SharedInformerFactory. type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory type sharedInformerFactory struct { client versioned.Interface namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc lock sync.Mutex defaultResync time.Duration customResync map[reflect.Type]time.Duration transform cache.TransformFunc informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[reflect.Type]bool // wg tracks how many goroutines were started. wg sync.WaitGroup // shuttingDown is true when Shutdown has been called. It may still be running // because it needs to wait for goroutines. shuttingDown bool } // WithCustomResyncConfig sets a custom resync period for the specified informer types. func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { for k, v := range resyncConfig { factory.customResync[reflect.TypeOf(k)] = v } return factory } } // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.tweakListOptions = tweakListOptions return factory } } // WithNamespace limits the SharedInformerFactory to the specified namespace. func WithNamespace(namespace string) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.namespace = namespace return factory } } // WithTransform sets a transform on all informers. func WithTransform(transform cache.TransformFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.transform = transform return factory } } // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync) } // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. // Listers obtained via this SharedInformerFactory will be subject to the same filters // as specified here. // Deprecated: Please use NewSharedInformerFactoryWithOptions instead func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) } // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { factory := &sharedInformerFactory{ client: client, namespace: v1.NamespaceAll, defaultResync: defaultResync, informers: make(map[reflect.Type]cache.SharedIndexInformer), startedInformers: make(map[reflect.Type]bool), customResync: make(map[reflect.Type]time.Duration), } // Apply all options for _, opt := range options { factory = opt(factory) } return factory } func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() if f.shuttingDown { return } for informerType, informer := range f.informers { if !f.startedInformers[informerType] { f.wg.Add(1) // We need a new variable in each loop iteration, // otherwise the goroutine would use the loop variable // and that keeps changing. informer := informer go func() { defer f.wg.Done() informer.Run(stopCh) }() f.startedInformers[informerType] = true } } } func (f *sharedInformerFactory) Shutdown() { f.lock.Lock() f.shuttingDown = true f.lock.Unlock() // Will return immediately if there is nothing to wait for. f.wg.Wait() } func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { informers := func() map[reflect.Type]cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informers := map[reflect.Type]cache.SharedIndexInformer{} for informerType, informer := range f.informers { if f.startedInformers[informerType] { informers[informerType] = informer } } return informers }() res := map[reflect.Type]bool{} for informType, informer := range informers { res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) } return res } // InformerFor returns the SharedIndexInformer for obj using an internal // client. func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informerType := reflect.TypeOf(obj) informer, exists := f.informers[informerType] if exists { return informer } resyncPeriod, exists := f.customResync[informerType] if !exists { resyncPeriod = f.defaultResync } informer = newFunc(f.client, resyncPeriod) informer.SetTransform(f.transform) f.informers[informerType] = informer return informer } // SharedInformerFactory provides shared informers for resources in all known // API group versions. // // It is typically used like this: // // ctx, cancel := context.Background() // defer cancel() // factory := NewSharedInformerFactory(client, resyncPeriod) // defer factory.WaitForStop() // Returns immediately if nothing was started. // genericInformer := factory.ForResource(resource) // typedInformer := factory.SomeAPIGroup().V1().SomeType() // factory.Start(ctx.Done()) // Start processing these informers. // synced := factory.WaitForCacheSync(ctx.Done()) // for v, ok := range synced { // if !ok { // fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) // return // } // } // // // Creating informers can also be created after Start, but then // // Start must be called again: // anotherGenericInformer := factory.ForResource(resource) // factory.Start(ctx.Done()) type SharedInformerFactory interface { internalinterfaces.SharedInformerFactory // Start initializes all requested informers. They are handled in goroutines // which run until the stop channel gets closed. // Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync. Start(stopCh <-chan struct{}) // Shutdown marks a factory as shutting down. At that point no new // informers can be started anymore and Start will return without // doing anything. // // In addition, Shutdown blocks until all goroutines have terminated. For that // to happen, the close channel(s) that they were started with must be closed, // either before Shutdown gets called or while it is waiting. // // Shutdown may be called multiple times, even concurrently. All such calls will // block until all goroutines have terminated. Shutdown() // WaitForCacheSync blocks until all started informers' caches were synced // or the stop channel gets closed. WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool // ForResource gives generic access to a shared informer of the matching type. ForResource(resource schema.GroupVersionResource) (GenericInformer, error) // InformerFor returns the SharedIndexInformer for obj using an internal // client. InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer Multicluster() apis.Interface } func (f *sharedInformerFactory) Multicluster() apis.Interface { return apis.New(f, f.namespace, f.tweakListOptions) } ================================================ FILE: pkg/client/informers/externalversions/generic.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package externalversions import ( fmt "fmt" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" v1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" v1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) // GenericInformer is type of SharedIndexInformer which will locate and delegate to other // sharedInformers based on type type GenericInformer interface { Informer() cache.SharedIndexInformer Lister() cache.GenericLister } type genericInformer struct { informer cache.SharedIndexInformer resource schema.GroupResource } // Informer returns the SharedIndexInformer. func (f *genericInformer) Informer() cache.SharedIndexInformer { return f.informer } // Lister returns the GenericLister. func (f *genericInformer) Lister() cache.GenericLister { return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) } // ForResource gives generic access to a shared informer of the matching type // TODO extend this to unknown resources with a client pool func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=multicluster.x-k8s.io, Version=v1alpha1 case v1alpha1.SchemeGroupVersion.WithResource("serviceexports"): return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1alpha1().ServiceExports().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("serviceimports"): return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1alpha1().ServiceImports().Informer()}, nil // Group=multicluster.x-k8s.io, Version=v1beta1 case v1beta1.SchemeGroupVersion.WithResource("serviceexports"): return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1beta1().ServiceExports().Informer()}, nil case v1beta1.SchemeGroupVersion.WithResource("serviceimports"): return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1beta1().ServiceImports().Informer()}, nil } return nil, fmt.Errorf("no informer found for %v", resource) } ================================================ FILE: pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go ================================================ /* Copyright 2020 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. */ // Code generated by informer-gen. DO NOT EDIT. package internalinterfaces import ( time "time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" cache "k8s.io/client-go/tools/cache" versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" ) // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer // SharedInformerFactory a small interface to allow for adding an informer without an import cycle type SharedInformerFactory interface { Start(stopCh <-chan struct{}) InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer } // TweakListOptionsFunc is a function that transforms a v1.ListOptions. type TweakListOptionsFunc func(*v1.ListOptions) ================================================ FILE: pkg/client/listers/apis/v1alpha1/expansion_generated.go ================================================ /* Copyright 2020 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. */ // Code generated by lister-gen. DO NOT EDIT. package v1alpha1 // ServiceExportListerExpansion allows custom methods to be added to // ServiceExportLister. type ServiceExportListerExpansion interface{} // ServiceExportNamespaceListerExpansion allows custom methods to be added to // ServiceExportNamespaceLister. type ServiceExportNamespaceListerExpansion interface{} // ServiceImportListerExpansion allows custom methods to be added to // ServiceImportLister. type ServiceImportListerExpansion interface{} // ServiceImportNamespaceListerExpansion allows custom methods to be added to // ServiceImportNamespaceLister. type ServiceImportNamespaceListerExpansion interface{} ================================================ FILE: pkg/client/listers/apis/v1alpha1/serviceexport.go ================================================ /* Copyright 2020 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. */ // Code generated by lister-gen. DO NOT EDIT. package v1alpha1 import ( labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" ) // ServiceExportLister helps list ServiceExports. // All objects returned here must be treated as read-only. type ServiceExportLister interface { // List lists all ServiceExports in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apisv1alpha1.ServiceExport, err error) // ServiceExports returns an object that can list and get ServiceExports. ServiceExports(namespace string) ServiceExportNamespaceLister ServiceExportListerExpansion } // serviceExportLister implements the ServiceExportLister interface. type serviceExportLister struct { listers.ResourceIndexer[*apisv1alpha1.ServiceExport] } // NewServiceExportLister returns a new ServiceExportLister. func NewServiceExportLister(indexer cache.Indexer) ServiceExportLister { return &serviceExportLister{listers.New[*apisv1alpha1.ServiceExport](indexer, apisv1alpha1.Resource("serviceexport"))} } // ServiceExports returns an object that can list and get ServiceExports. func (s *serviceExportLister) ServiceExports(namespace string) ServiceExportNamespaceLister { return serviceExportNamespaceLister{listers.NewNamespaced[*apisv1alpha1.ServiceExport](s.ResourceIndexer, namespace)} } // ServiceExportNamespaceLister helps list and get ServiceExports. // All objects returned here must be treated as read-only. type ServiceExportNamespaceLister interface { // List lists all ServiceExports in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apisv1alpha1.ServiceExport, err error) // Get retrieves the ServiceExport from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*apisv1alpha1.ServiceExport, error) ServiceExportNamespaceListerExpansion } // serviceExportNamespaceLister implements the ServiceExportNamespaceLister // interface. type serviceExportNamespaceLister struct { listers.ResourceIndexer[*apisv1alpha1.ServiceExport] } ================================================ FILE: pkg/client/listers/apis/v1alpha1/serviceimport.go ================================================ /* Copyright 2020 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. */ // Code generated by lister-gen. DO NOT EDIT. package v1alpha1 import ( labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" ) // ServiceImportLister helps list ServiceImports. // All objects returned here must be treated as read-only. type ServiceImportLister interface { // List lists all ServiceImports in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apisv1alpha1.ServiceImport, err error) // ServiceImports returns an object that can list and get ServiceImports. ServiceImports(namespace string) ServiceImportNamespaceLister ServiceImportListerExpansion } // serviceImportLister implements the ServiceImportLister interface. type serviceImportLister struct { listers.ResourceIndexer[*apisv1alpha1.ServiceImport] } // NewServiceImportLister returns a new ServiceImportLister. func NewServiceImportLister(indexer cache.Indexer) ServiceImportLister { return &serviceImportLister{listers.New[*apisv1alpha1.ServiceImport](indexer, apisv1alpha1.Resource("serviceimport"))} } // ServiceImports returns an object that can list and get ServiceImports. func (s *serviceImportLister) ServiceImports(namespace string) ServiceImportNamespaceLister { return serviceImportNamespaceLister{listers.NewNamespaced[*apisv1alpha1.ServiceImport](s.ResourceIndexer, namespace)} } // ServiceImportNamespaceLister helps list and get ServiceImports. // All objects returned here must be treated as read-only. type ServiceImportNamespaceLister interface { // List lists all ServiceImports in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apisv1alpha1.ServiceImport, err error) // Get retrieves the ServiceImport from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*apisv1alpha1.ServiceImport, error) ServiceImportNamespaceListerExpansion } // serviceImportNamespaceLister implements the ServiceImportNamespaceLister // interface. type serviceImportNamespaceLister struct { listers.ResourceIndexer[*apisv1alpha1.ServiceImport] } ================================================ FILE: pkg/client/listers/apis/v1beta1/expansion_generated.go ================================================ /* Copyright 2020 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. */ // Code generated by lister-gen. DO NOT EDIT. package v1beta1 // ServiceExportListerExpansion allows custom methods to be added to // ServiceExportLister. type ServiceExportListerExpansion interface{} // ServiceExportNamespaceListerExpansion allows custom methods to be added to // ServiceExportNamespaceLister. type ServiceExportNamespaceListerExpansion interface{} // ServiceImportListerExpansion allows custom methods to be added to // ServiceImportLister. type ServiceImportListerExpansion interface{} // ServiceImportNamespaceListerExpansion allows custom methods to be added to // ServiceImportNamespaceLister. type ServiceImportNamespaceListerExpansion interface{} ================================================ FILE: pkg/client/listers/apis/v1beta1/serviceexport.go ================================================ /* Copyright 2020 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. */ // Code generated by lister-gen. DO NOT EDIT. package v1beta1 import ( labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) // ServiceExportLister helps list ServiceExports. // All objects returned here must be treated as read-only. type ServiceExportLister interface { // List lists all ServiceExports in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apisv1beta1.ServiceExport, err error) // ServiceExports returns an object that can list and get ServiceExports. ServiceExports(namespace string) ServiceExportNamespaceLister ServiceExportListerExpansion } // serviceExportLister implements the ServiceExportLister interface. type serviceExportLister struct { listers.ResourceIndexer[*apisv1beta1.ServiceExport] } // NewServiceExportLister returns a new ServiceExportLister. func NewServiceExportLister(indexer cache.Indexer) ServiceExportLister { return &serviceExportLister{listers.New[*apisv1beta1.ServiceExport](indexer, apisv1beta1.Resource("serviceexport"))} } // ServiceExports returns an object that can list and get ServiceExports. func (s *serviceExportLister) ServiceExports(namespace string) ServiceExportNamespaceLister { return serviceExportNamespaceLister{listers.NewNamespaced[*apisv1beta1.ServiceExport](s.ResourceIndexer, namespace)} } // ServiceExportNamespaceLister helps list and get ServiceExports. // All objects returned here must be treated as read-only. type ServiceExportNamespaceLister interface { // List lists all ServiceExports in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apisv1beta1.ServiceExport, err error) // Get retrieves the ServiceExport from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*apisv1beta1.ServiceExport, error) ServiceExportNamespaceListerExpansion } // serviceExportNamespaceLister implements the ServiceExportNamespaceLister // interface. type serviceExportNamespaceLister struct { listers.ResourceIndexer[*apisv1beta1.ServiceExport] } ================================================ FILE: pkg/client/listers/apis/v1beta1/serviceimport.go ================================================ /* Copyright 2020 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. */ // Code generated by lister-gen. DO NOT EDIT. package v1beta1 import ( labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" apisv1beta1 "sigs.k8s.io/mcs-api/pkg/apis/v1beta1" ) // ServiceImportLister helps list ServiceImports. // All objects returned here must be treated as read-only. type ServiceImportLister interface { // List lists all ServiceImports in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apisv1beta1.ServiceImport, err error) // ServiceImports returns an object that can list and get ServiceImports. ServiceImports(namespace string) ServiceImportNamespaceLister ServiceImportListerExpansion } // serviceImportLister implements the ServiceImportLister interface. type serviceImportLister struct { listers.ResourceIndexer[*apisv1beta1.ServiceImport] } // NewServiceImportLister returns a new ServiceImportLister. func NewServiceImportLister(indexer cache.Indexer) ServiceImportLister { return &serviceImportLister{listers.New[*apisv1beta1.ServiceImport](indexer, apisv1beta1.Resource("serviceimport"))} } // ServiceImports returns an object that can list and get ServiceImports. func (s *serviceImportLister) ServiceImports(namespace string) ServiceImportNamespaceLister { return serviceImportNamespaceLister{listers.NewNamespaced[*apisv1beta1.ServiceImport](s.ResourceIndexer, namespace)} } // ServiceImportNamespaceLister helps list and get ServiceImports. // All objects returned here must be treated as read-only. type ServiceImportNamespaceLister interface { // List lists all ServiceImports in the indexer for a given namespace. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apisv1beta1.ServiceImport, err error) // Get retrieves the ServiceImport from the indexer for a given namespace and name. // Objects returned here must be treated as read-only. Get(name string) (*apisv1beta1.ServiceImport, error) ServiceImportNamespaceListerExpansion } // serviceImportNamespaceLister implements the ServiceImportNamespaceLister // interface. type serviceImportNamespaceLister struct { listers.ResourceIndexer[*apisv1beta1.ServiceImport] } ================================================ FILE: scripts/.gitignore ================================================ *.kubeconfig *.tmp ================================================ FILE: scripts/c1.yaml ================================================ kind: Cluster apiVersion: "kind.x-k8s.io/v1alpha4" networking: podSubnet: "10.10.0.0/16" serviceSubnet: "10.11.0.0/16" nodes: - role: control-plane kubeadmConfigPatches: - | kind: ClusterConfiguration dns: # TODO: Remove this after Kubernetes 1.35. # Reference: https://github.com/kubernetes/kubernetes/pull/132288 imageTag: v1.12.2 ================================================ FILE: scripts/c2.yaml ================================================ kind: Cluster apiVersion: "kind.x-k8s.io/v1alpha4" networking: podSubnet: "10.12.0.0/16" serviceSubnet: "10.13.0.0/16" nodes: - role: control-plane kubeadmConfigPatches: - | kind: ClusterConfiguration dns: # TODO: Remove this after Kubernetes 1.35. # Reference: https://github.com/kubernetes/kubernetes/pull/132288 imageTag: v1.12.2 ================================================ FILE: scripts/coredns-rbac.json ================================================ [ { "op": "add", "path": "/rules/-", "value": { "apiGroups": [ "multicluster.x-k8s.io" ], "resources": [ "serviceimports" ], "verbs": [ "list", "watch" ] } } ] ================================================ FILE: scripts/down.sh ================================================ #!/bin/bash # Copyright 2020 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 -e cd $(dirname ${BASH_SOURCE}) kind() { go -C ../controllers run sigs.k8s.io/kind "$@" } c1=${CLUSTER1:-c1} c2=${CLUSTER2:-c2} kind delete cluster --name "${c1}" kind delete cluster --name "${c2}" ================================================ FILE: scripts/e2e-test.sh ================================================ #!/bin/bash # Copyright 2020 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. cd $(dirname ${BASH_SOURCE}) set -e export KUBECONFIG1=$(mktemp --suffix=".kubeconfig") export KUBECONFIG2=$(mktemp --suffix=".kubeconfig") function cleanup() { if [ -z "${NO_TEAR_DOWN}" ]; then ./down.sh rm ${KUBECONFIG1} rm ${KUBECONFIG2} else echo "KUBECONFIG1=${KUBECONFIG1}" echo "KUBECONFIG2=${KUBECONFIG2}" fi } trap cleanup EXIT ./up.sh go -C ../e2e run github.com/onsi/ginkgo/v2/ginkgo . ================================================ FILE: scripts/up.sh ================================================ #!/bin/bash # Copyright 2020 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. cd $(dirname ${BASH_SOURCE}) . ./util.sh set -e kind() { go -C ../controllers run sigs.k8s.io/kind "$@" } c1=${CLUSTER1:-c1} c2=${CLUSTER2:-c2} kubeconfig1=${KUBECONFIG1:-"$PWD/${c1}.kubeconfig"} kubeconfig2=${KUBECONFIG2:-"$PWD/${c2}.kubeconfig"} controller_image=${MCS_CONTROLLER_IMAGE:-"mcs-api-controller"} k1="kubectl --kubeconfig ${kubeconfig1}" k2="kubectl --kubeconfig ${kubeconfig2}" if [ ! -z "${BUILD_CONTROLLER}" ] || [ -z "$(docker images mcs-api-controller -q)" ]; then pushd ../ make docker-build popd fi kind create cluster --name "${c1}" --config "$PWD/${c1}.yaml" kind create cluster --name "${c2}" --config "$PWD/${c2}.yaml" kind get kubeconfig --name "${c1}" > "${kubeconfig1}" kind get kubeconfig --name "${c2}" > "${kubeconfig2}" kind load docker-image "${controller_image}" --name "${c1}" kind load docker-image "${controller_image}" --name "${c2}" echo "Configuring CoreDNS" function update_coredns() { kubectl --kubeconfig ${1} apply -f ../config/crd kubectl --kubeconfig ${1} patch clusterrole system:coredns --type json --patch-file coredns-rbac.json # Patching Corefile based on Cilium documentation: https://docs.cilium.io/en/latest/network/clustermesh/mcsapi/#prerequisites kubectl --kubeconfig ${1} get configmap -n kube-system coredns -o yaml | \ sed -e 's/cluster\.local/cluster.local clusterset.local/g' | \ sed -E 's/^(.*)kubernetes(.*)\{/\1kubernetes\2{\n\1 multicluster clusterset.local/' | \ kubectl --kubeconfig ${1} replace -f- kubectl --kubeconfig ${1} rollout restart deploy -n kube-system coredns } update_coredns ${kubeconfig1} update_coredns ${kubeconfig2} function pod_cidrs() { kubectl --kubeconfig "${1}" get nodes -o jsonpath='{range .items[*]}{.spec.podCIDR}{"\n"}' } function add_routes() { unset IFS routes=$(kubectl --kubeconfig ${3} get node ${2} -o jsonpath='ip route add {.spec.podCIDR} via {.status.addresses[?(.type=="InternalIP")].address}') echo "Connecting cluster ${1} to ${2}" IFS=$'\n' for n in $(kind get nodes --name "${1}"); do for r in $routes; do eval "docker exec $n $r" done done unset IFS } waitfor pod_cidrs ${kubeconfig1} waitfor pod_cidrs ${kubeconfig2} echo "Connecting cluster networks..." add_routes "${c1}" "${c2}-control-plane" "${kubeconfig2}" add_routes "${c2}" "${c1}-control-plane" "${kubeconfig1}" echo "Cluster networks connected" ${k1} apply -f ../config/rbac ${k2} apply -f ../config/rbac ${k1} create sa mcs-api-controller ${k1} create clusterrolebinding mcs-api-binding --clusterrole=mcs-derived-service-manager --serviceaccount=default:mcs-api-controller ${k1} run --image "${controller_image}" --image-pull-policy=Never mcs-api-controller --overrides='{ "spec": { "serviceAccount": "mcs-api-controller" } }' ${k2} create sa mcs-api-controller ${k2} create clusterrolebinding mcs-api-binding --clusterrole=mcs-derived-service-manager --serviceaccount=default:mcs-api-controller ${k2} run --image "${controller_image}" --image-pull-policy=Never mcs-api-controller --overrides='{ "spec": { "serviceAccount": "mcs-api-controller" } }' ================================================ FILE: scripts/util.sh ================================================ #!/bin/bash # Copyright 2020 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. function waitfor() { for i in {1..30}; do if [ ! -z "$(${@})" ]; then break fi sleep 1 done if [ -z "$(${@})" ]; then echo "No results for '${1}' after 30 attempts" fi } ================================================ FILE: tools/go.mod ================================================ module github.com/kubernetes-sigs/mcs-api/tools go 1.23.0 require ( golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 k8s.io/code-generator v0.32.5 sigs.k8s.io/controller-tools v0.17.3 ) require ( github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/mod v0.23.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.2 // indirect k8s.io/apiextensions-apiserver v0.32.2 // indirect k8s.io/apimachinery v0.32.5 // indirect k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) ================================================ FILE: tools/go.sum ================================================ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM= k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/code-generator v0.32.5 h1:dvoXgaWTDPLsg0txUzWj5xPV8UwHOsBhmm4JC9Gd1Qo= k8s.io/code-generator v0.32.5/go.mod h1:7S6jUv4ZAnI2yDUJUQUEuc3gv6+qFhnkB5Fhs9Eb0d8= k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4= k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3EwfNw= sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= ================================================ FILE: tools/tools.go ================================================ //go:build tools // +build tools /* Copyright 2020 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. */ // This package contains import references to packages required only for the // build process. // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module package tools import ( _ "golang.org/x/lint/golint" _ "k8s.io/code-generator/cmd/client-gen" _ "k8s.io/code-generator/cmd/deepcopy-gen" _ "k8s.io/code-generator/cmd/informer-gen" _ "k8s.io/code-generator/cmd/lister-gen" _ "k8s.io/code-generator/cmd/register-gen" _ "sigs.k8s.io/controller-tools/cmd/controller-gen" )