├── demo ├── .gitignore ├── yaml │ ├── svc.yaml │ ├── serviceimport.yaml │ ├── serviceimport-with-vip.yaml │ ├── dep1.yaml │ └── dep2.yaml ├── edit-meta ├── reset.sh ├── udemo.sh └── demo.sh ├── scripts ├── .gitignore ├── coredns-rbac.json ├── c1.yaml ├── c2.yaml ├── util.sh ├── down.sh ├── e2e-test.sh └── up.sh ├── code-of-conduct.md ├── OWNERS ├── .github └── workflows │ └── auto-label.yml ├── RELEASE.md ├── SECURITY_CONTACTS ├── hack ├── boilerplate.go.txt ├── boilerplate │ ├── boilerplate.go.txt │ ├── boilerplate.sh.txt │ ├── boilerplate.py.txt │ └── boilerplate.py ├── verify-codegen.sh ├── verify-boilerplate.sh ├── verify-crd-bump-revision.sh ├── update-k8s.sh ├── verify-gofmt.sh ├── verify-golint.sh ├── verify-all.sh ├── kube-env.sh ├── verify-crds.sh └── update-codegen.sh ├── config ├── rbac │ └── role.yaml ├── crd │ ├── embed.go │ └── multicluster.x-k8s.io_serviceexports.yaml └── crd-base │ ├── multicluster.x-k8s.io_serviceexports.yaml │ └── multicluster.x-k8s.io_serviceimports.yaml ├── .gitignore ├── pkg ├── client │ ├── clientset │ │ └── versioned │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ └── clientset_generated.go │ │ │ ├── typed │ │ │ └── apis │ │ │ │ └── v1alpha1 │ │ │ │ ├── fake │ │ │ │ ├── doc.go │ │ │ │ ├── fake_apis_client.go │ │ │ │ ├── fake_serviceexport.go │ │ │ │ └── fake_serviceimport.go │ │ │ │ ├── doc.go │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── serviceexport.go │ │ │ │ ├── serviceimport.go │ │ │ │ └── apis_client.go │ │ │ ├── scheme │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ └── clientset.go │ ├── listers │ │ └── apis │ │ │ └── v1alpha1 │ │ │ ├── expansion_generated.go │ │ │ ├── serviceexport.go │ │ │ └── serviceimport.go │ └── informers │ │ └── externalversions │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ ├── apis │ │ ├── interface.go │ │ └── v1alpha1 │ │ │ ├── interface.go │ │ │ ├── serviceexport.go │ │ │ └── serviceimport.go │ │ └── generic.go └── apis │ └── v1alpha1 │ ├── doc.go │ ├── well_known_labels.go │ ├── BUILD │ └── zz_generated.register.go ├── Dockerfile ├── conformance ├── conformance_suite_test.go ├── report_template.gohtml ├── framework.go ├── go.mod ├── connectivity.go ├── endpoint_slice.go ├── k8s_objects.go └── clusterip_service_dns.go ├── SECURITY.md ├── tools ├── tools.go └── go.mod ├── CONTRIBUTING.md ├── README.md ├── go.mod ├── controllers ├── cmd │ └── servicecontroller │ │ └── servicecontroller.go ├── endpointslice.go ├── common.go ├── service.go ├── controllers_suite_test.go ├── go.mod ├── endpointslice_test.go ├── serviceimport.go └── serviceimport_test.go ├── e2e ├── go.mod ├── e2e_suite_test.go └── localserviceimpact_test.go └── Makefile /demo/.gitignore: -------------------------------------------------------------------------------- 1 | *.kubeconfig 2 | *.tmp -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | *.kubeconfig 2 | *.tmp -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /demo/yaml/svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: serve 5 | namespace: demo 6 | spec: 7 | ports: 8 | - port: 80 9 | targetPort: 8080 10 | selector: 11 | app: serve 12 | -------------------------------------------------------------------------------- /demo/yaml/serviceimport.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: multicluster.x-k8s.io/v1alpha1 2 | kind: ServiceImport 3 | metadata: 4 | name: serve 5 | namespace: demo 6 | spec: 7 | type: ClusterSetIP 8 | ports: 9 | - port: 80 10 | protocol: TCP 11 | -------------------------------------------------------------------------------- /demo/yaml/serviceimport-with-vip.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: multicluster.x-k8s.io/v1alpha1 2 | kind: ServiceImport 3 | metadata: 4 | name: serve-with-vip 5 | namespace: demo 6 | spec: 7 | type: ClusterSetIP 8 | ips: 9 | - 1.2.3.4 10 | ports: 11 | - port: 80 12 | protocol: TCP 13 | -------------------------------------------------------------------------------- /scripts/coredns-rbac.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "op": "add", 4 | "path": "/rules/-", 5 | "value": { 6 | "apiGroups": [ 7 | "multicluster.x-k8s.io" 8 | ], 9 | "resources": [ 10 | "serviceimports" 11 | ], 12 | "verbs": [ 13 | "list", 14 | "watch" 15 | ] 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | reviewers: 4 | - jeremyot 5 | - lauralorenz 6 | - skitt 7 | - MrFreezeex 8 | - munnerz 9 | - RainbowMango 10 | - ryanzhang-oss 11 | # Pending org membership 12 | # - corentone 13 | # - jnpacker 14 | 15 | approvers: 16 | - JeremyOT 17 | - lauralorenz 18 | - skitt 19 | 20 | emeritus_approvers: 21 | - pmorie 22 | -------------------------------------------------------------------------------- /scripts/c1.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: "kind.x-k8s.io/v1alpha4" 3 | networking: 4 | podSubnet: "10.10.0.0/16" 5 | serviceSubnet: "10.11.0.0/16" 6 | nodes: 7 | - role: control-plane 8 | kubeadmConfigPatches: 9 | - | 10 | kind: ClusterConfiguration 11 | dns: 12 | # TODO: Remove this after Kubernetes 1.35. 13 | # Reference: https://github.com/kubernetes/kubernetes/pull/132288 14 | imageTag: v1.12.2 15 | -------------------------------------------------------------------------------- /scripts/c2.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: "kind.x-k8s.io/v1alpha4" 3 | networking: 4 | podSubnet: "10.12.0.0/16" 5 | serviceSubnet: "10.13.0.0/16" 6 | nodes: 7 | - role: control-plane 8 | kubeadmConfigPatches: 9 | - | 10 | kind: ClusterConfiguration 11 | dns: 12 | # TODO: Remove this after Kubernetes 1.35. 13 | # Reference: https://github.com/kubernetes/kubernetes/pull/132288 14 | imageTag: v1.12.2 15 | -------------------------------------------------------------------------------- /.github/workflows/auto-label.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Label issues 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | jobs: 9 | label_issues: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | steps: 14 | - run: gh issue edit "$NUMBER" --add-label "$LABELS" 15 | env: 16 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | GH_REPO: ${{ github.repository }} 18 | NUMBER: ${{ github.event.issue.number }} 19 | LABELS: sig/multicluster 20 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | The Kubernetes Template Project is released on an as-needed basis. The process is as follows: 4 | 5 | 1. An issue is proposing a new release with a changelog since the last release 6 | 1. All [OWNERS](OWNERS) must LGTM this release 7 | 1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` 8 | 1. The release issue is closed 9 | 1. An announcement email is sent to `kubernetes-dev@googlegroups.com` with the subject `[ANNOUNCE] kubernetes-template-project $VERSION is released` 10 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | pmorie 14 | JeremyOT 15 | -------------------------------------------------------------------------------- /demo/edit-meta: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import argparse 5 | import yaml 6 | 7 | if __name__ == '__main__': 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument('-m', '--metadata', dest='metadata', help="Overwrite the resource's metadata with this value, may be any valid YAML") 10 | args = parser.parse_args() 11 | 12 | data = yaml.load(sys.stdin, Loader=yaml.Loader) 13 | if args.metadata: 14 | value = yaml.load(args.metadata, Loader=yaml.Loader) 15 | data['metadata'] = value 16 | 17 | print(yaml.dump(data, indent=2, default_flow_style=False, Dumper=yaml.Dumper)) 18 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright YEAR The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.sh.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: mcs-derived-service-manager 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - services 11 | - services/status 12 | verbs: 13 | - create 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - discovery.k8s.io 21 | resources: 22 | - endpointslices 23 | verbs: 24 | - get 25 | - list 26 | - patch 27 | - update 28 | - watch 29 | - apiGroups: 30 | - multicluster.x-k8s.io 31 | resources: 32 | - serviceimports 33 | verbs: 34 | - get 35 | - list 36 | - patch 37 | - update 38 | - watch 39 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.py.txt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright YEAR The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .\#* 3 | ._* 4 | \#*\# 5 | /_artifacts/ 6 | /bazel-* 7 | bin 8 | .classpath 9 | /cluster 10 | /.config/gcloud*/ 11 | *.dll 12 | /doc_tmp/ 13 | !\.drone\.sec 14 | .DS_Store 15 | *.dylib 16 | .envrc 17 | *.exe 18 | *.exe~ 19 | /_gopath/ 20 | /.gsutil/ 21 | /hack/.test-cmd-auth 22 | **/.hg* 23 | .idea/ 24 | *.iml 25 | /junit*.xml 26 | /.make/ 27 | .netrwhist 28 | network_closure.sh 29 | *.out 30 | /_output*/ 31 | /output*/ 32 | .project 33 | *.pyc 34 | [._]*.s[a-w][a-z] 35 | [._]s[a-w][a-z] 36 | Session.vim 37 | .settings/** 38 | *.so 39 | *.swo 40 | *.swp 41 | .tags* 42 | *.test 43 | /third_party/pkg 44 | .*.timestamp 45 | /_tmp/ 46 | *.un~ 47 | .vagrant 48 | !vendor/**/zz_generated.* 49 | .vscode 50 | /www/test_out 51 | report.html 52 | .*.timestamp 53 | -------------------------------------------------------------------------------- /demo/yaml/dep1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: serve 5 | namespace: demo 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: serve 11 | template: 12 | metadata: 13 | labels: 14 | app: serve 15 | spec: 16 | containers: 17 | - name: serve 18 | image: jeremyot/serve:0a40de8 19 | args: 20 | - "--message='hello from cluster 1 (Node: {{env \"NODE_NAME\"}} Pod: {{env \"POD_NAME\"}} Address: {{addr}})'" 21 | env: 22 | - name: NODE_NAME 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: spec.nodeName 26 | - name: POD_NAME 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.name -------------------------------------------------------------------------------- /demo/yaml/dep2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: serve 5 | namespace: demo 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: serve 11 | template: 12 | metadata: 13 | labels: 14 | app: serve 15 | spec: 16 | containers: 17 | - name: serve 18 | image: jeremyot/serve:0a40de8 19 | args: 20 | - "--message='hello from cluster 2 (Node: {{env \"NODE_NAME\"}} Pod: {{env \"POD_NAME\"}} Address: {{addr}})'" 21 | env: 22 | - name: NODE_NAME 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: spec.nodeName 26 | - name: POD_NAME 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.name -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated fake clientset. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // Package fake has the automatically generated clients. 20 | package fake 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.23 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | RUN go mod download 9 | 10 | # Copy the go source 11 | COPY controllers/ controllers/ 12 | RUN go -C controllers mod download 13 | COPY pkg/ pkg/ 14 | 15 | # Build 16 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go -C controllers build -a -o /workspace/controller cmd/servicecontroller/servicecontroller.go 17 | 18 | # Use distroless as minimal base image to package the manager binary 19 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 20 | FROM gcr.io/distroless/static:nonroot 21 | WORKDIR / 22 | COPY --from=builder /workspace/controller . 23 | USER nonroot:nonroot 24 | 25 | ENTRYPOINT ["/controller"] 26 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package contains the scheme of the automatically generated clientset. 20 | package scheme 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated typed clients. 20 | package v1alpha1 21 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API schema definitions for the Multi-Cluster 18 | // Services v1alpha1 API group. 19 | // +kubebuilder:object:generate=true 20 | // +groupName=multicluster.x-k8s.io 21 | package v1alpha1 22 | -------------------------------------------------------------------------------- /hack/verify-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. 22 | 23 | cd $SCRIPT_ROOT 24 | VERIFY_CODEGEN=true $SCRIPT_ROOT/hack/update-codegen.sh 25 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | type ServiceExportExpansion interface{} 22 | 23 | type ServiceImportExpansion interface{} 24 | -------------------------------------------------------------------------------- /demo/reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | cd $(dirname ${BASH_SOURCE}) 18 | . ./udemo.sh 19 | 20 | c1=c1 21 | c2=c2 22 | k1="kubectl --kubeconfig ${c1}.kubeconfig" 23 | k2="kubectl --kubeconfig ${c2}.kubeconfig" 24 | 25 | ${k1} delete ns demo 26 | ${k2} delete ns demo -------------------------------------------------------------------------------- /scripts/util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | function waitfor() { 18 | for i in {1..30}; do 19 | if [ ! -z "$(${@})" ]; then 20 | break 21 | fi 22 | sleep 1 23 | done 24 | if [ -z "$(${@})" ]; then 25 | echo "No results for '${1}' after 30 attempts" 26 | fi 27 | } -------------------------------------------------------------------------------- /conformance/conformance_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package conformance_test 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo/v2" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestConformance(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Conformance Suite") 29 | } 30 | -------------------------------------------------------------------------------- /scripts/down.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | cd $(dirname ${BASH_SOURCE}) 20 | 21 | kind() { 22 | go -C ../controllers run sigs.k8s.io/kind "$@" 23 | } 24 | 25 | c1=${CLUSTER1:-c1} 26 | c2=${CLUSTER2:-c2} 27 | 28 | kind delete cluster --name "${c1}" 29 | kind delete cluster --name "${c2}" 30 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/well_known_labels.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1alpha1 18 | 19 | const ( 20 | // LabelServiceName is used to indicate the name of multi-cluster service 21 | // that an EndpointSlice belongs to. 22 | LabelServiceName = "multicluster.kubernetes.io/service-name" 23 | 24 | // LabelSourceCluster is used to indicate the name of the cluster in which an exported resource exists. 25 | LabelSourceCluster = "multicluster.kubernetes.io/source-cluster" 26 | ) 27 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security Announcements 4 | 5 | Join the [kubernetes-security-announce] group for security and vulnerability announcements. 6 | 7 | You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss]. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Instructions for reporting a vulnerability can be found on the 12 | [Kubernetes Security and Disclosure Information] page. 13 | 14 | ## Supported Versions 15 | 16 | Information about supported Kubernetes versions can be found on the 17 | [Kubernetes version and version skew support policy] page on the Kubernetes website. 18 | 19 | [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce 20 | [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 21 | [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions 22 | [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability 23 | -------------------------------------------------------------------------------- /conformance/report_template.gohtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MCS API conformance report 6 | 14 | 15 | 16 |

MCS Conformance Report

17 | 18 | {{if .SuiteFailure }} 19 |

{{.SuiteFailure}}

20 | {{end}} 21 | 22 | {{range .Groups}} 23 |

{{.Name}}

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{range .Tests}} 32 | 33 | {{ if .Failed }} 34 | 35 | {{ else if .Conformant }} 36 | 37 | {{ else }} 38 | 39 | {{end}} 40 | 41 | 42 | {{end}} 43 |
ConformantDescription
Unknown{{.Message}}Yes{{.Message}}No{{.Message}}{{.Desc}}
44 | {{end}} 45 | 46 | -------------------------------------------------------------------------------- /hack/verify-boilerplate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2014 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. 22 | 23 | boilerDir="${SCRIPT_ROOT}/hack/boilerplate" 24 | boiler="${boilerDir}/boilerplate.py" 25 | 26 | files_need_boilerplate=($(${boiler} "$@")) 27 | 28 | # Run boilerplate check 29 | if [[ ${#files_need_boilerplate[@]} -gt 0 ]]; then 30 | for file in "${files_need_boilerplate[@]}"; do 31 | echo "Boilerplate header is wrong for: ${file}" 32 | done 33 | 34 | exit 1 35 | fi 36 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = [ 6 | "doc.go", 7 | "register.go", 8 | "types.go", 9 | "well_known_labels.go", 10 | "zz_generated.deepcopy.go", 11 | ], 12 | importmap = "k8s.io/kubernetes/vendor/k8s.io/mcs-api/pkg/apis/v1alpha1", 13 | importpath = "k8s.io/mcs-api/pkg/apis/v1alpha1", 14 | visibility = ["//visibility:public"], 15 | deps = [ 16 | "//staging/src/k8s.io/api/core/v1:go_default_library", 17 | "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", 18 | "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", 19 | "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", 20 | ], 21 | ) 22 | 23 | filegroup( 24 | name = "package-srcs", 25 | srcs = glob(["**"]), 26 | tags = ["automanaged"], 27 | visibility = ["//visibility:private"], 28 | ) 29 | 30 | filegroup( 31 | name = "all-srcs", 32 | srcs = [":package-srcs"], 33 | tags = ["automanaged"], 34 | visibility = ["//visibility:public"], 35 | ) 36 | -------------------------------------------------------------------------------- /scripts/e2e-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | cd $(dirname ${BASH_SOURCE}) 18 | 19 | set -e 20 | 21 | export KUBECONFIG1=$(mktemp --suffix=".kubeconfig") 22 | export KUBECONFIG2=$(mktemp --suffix=".kubeconfig") 23 | 24 | function cleanup() { 25 | if [ -z "${NO_TEAR_DOWN}" ]; then 26 | ./down.sh 27 | rm ${KUBECONFIG1} 28 | rm ${KUBECONFIG2} 29 | else 30 | echo "KUBECONFIG1=${KUBECONFIG1}" 31 | echo "KUBECONFIG2=${KUBECONFIG2}" 32 | fi 33 | } 34 | 35 | trap cleanup EXIT 36 | 37 | ./up.sh 38 | go -C ../e2e run github.com/onsi/ginkgo/v2/ginkgo . 39 | -------------------------------------------------------------------------------- /hack/verify-crd-bump-revision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2025 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Check against master if PULL_BASE_SHA is not defined by prow 18 | BASE_REF="${PULL_BASE_SHA:-master}" 19 | 20 | crd_changed="$(git diff --name-only "${BASE_REF}" | grep -c "^config/crd/.*\.yaml$")" 21 | version_label_changed="$(git diff -U0 "${BASE_REF}" -- "config/crd-base/" | grep -c "multicluster.x-k8s.io/crd-schema-revision")" 22 | 23 | if [ "${crd_changed}" -gt 0 ] && [ "${version_label_changed}" -ne 4 ]; then 24 | echo "❌ CRDs were modified, but the CRD revision labels were not changed in 'config/crd-base/'. Please bump the CRDs revision." 25 | exit 1 26 | fi 27 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | /* 5 | Copyright 2020 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // This package contains import references to packages required only for the 21 | // build process. 22 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 23 | package tools 24 | 25 | import ( 26 | _ "golang.org/x/lint/golint" 27 | _ "k8s.io/code-generator/cmd/client-gen" 28 | _ "k8s.io/code-generator/cmd/deepcopy-gen" 29 | _ "k8s.io/code-generator/cmd/informer-gen" 30 | _ "k8s.io/code-generator/cmd/lister-gen" 31 | _ "k8s.io/code-generator/cmd/register-gen" 32 | _ "sigs.k8s.io/controller-tools/cmd/controller-gen" 33 | ) 34 | -------------------------------------------------------------------------------- /hack/update-k8s.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # This script updates mcs-api dependencies to either the latest 18 | # k8s.io dependencies, or to the version given as argument 19 | # (with the @, e.g. @v0.32.5). 20 | 21 | set -o errexit 22 | set -o nounset 23 | set -o pipefail 24 | 25 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. 26 | 27 | for dir in "$SCRIPT_ROOT"{,/tools}; do 28 | awk '/[^.]k8s.io[/][^ ]+ v[.0-9]+$/ { print $1 "'"$1"'" }' "$dir/go.mod" | 29 | xargs -r -n 1 go -C "$dir" get 30 | done 31 | 32 | for mod in "$SCRIPT_ROOT"/go.mod "$SCRIPT_ROOT"/*/go.mod; do 33 | go -C "${mod%/*}" mod tidy 34 | done 35 | 36 | make generate 37 | -------------------------------------------------------------------------------- /hack/verify-gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2014 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # GoFmt apparently is changing @ head... 18 | 19 | set -o errexit 20 | set -o nounset 21 | set -o pipefail 22 | 23 | KUBE_ROOT=$(dirname "${BASH_SOURCE}")/.. 24 | 25 | cd "${KUBE_ROOT}" 26 | 27 | find_files() { 28 | find . -not \( \ 29 | \( \ 30 | -wholename './.git' \ 31 | -o -wholename '*/vendor/*' \ 32 | \) -prune \ 33 | \) -name '*.go' 34 | } 35 | 36 | GOFMT="gofmt -s" 37 | bad_files=$(find_files | xargs $GOFMT -l) 38 | if [[ -n "${bad_files}" ]]; then 39 | echo "!!! '$GOFMT' needs to be run on the following files: " 40 | echo "${bad_files}" 41 | exit 1 42 | fi 43 | -------------------------------------------------------------------------------- /config/crd/embed.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package crd 18 | 19 | import _ "embed" // import embed to be able to use go:embed 20 | 21 | var ( 22 | // ServiceExportCRD is the embedded YAML for the ServiceExport CRD 23 | //go:embed multicluster.x-k8s.io_serviceexports.yaml 24 | ServiceExportCRD []byte 25 | // ServiceImportCRD is the embedded YAML for the ServiceImport CRD 26 | //go:embed multicluster.x-k8s.io_serviceimports.yaml 27 | ServiceImportCRD []byte 28 | ) 29 | 30 | const ( 31 | // ReleaseVersionLabel is the label which indicate the release version 32 | ReleaseVersionLabel = "multicluster.x-k8s.io/release-version" 33 | // CustomResourceDefinitionSchemaRevisionLabel is the label which holds the CRD schema revision 34 | CustomResourceDefinitionSchemaRevisionLabel = "multicluster.x-k8s.io/crd-schema-revision" 35 | ) 36 | -------------------------------------------------------------------------------- /pkg/client/listers/apis/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | // ServiceExportListerExpansion allows custom methods to be added to 22 | // ServiceExportLister. 23 | type ServiceExportListerExpansion interface{} 24 | 25 | // ServiceExportNamespaceListerExpansion allows custom methods to be added to 26 | // ServiceExportNamespaceLister. 27 | type ServiceExportNamespaceListerExpansion interface{} 28 | 29 | // ServiceImportListerExpansion allows custom methods to be added to 30 | // ServiceImportLister. 31 | type ServiceImportListerExpansion interface{} 32 | 33 | // ServiceImportNamespaceListerExpansion allows custom methods to be added to 34 | // ServiceImportNamespaceLister. 35 | type ServiceImportNamespaceListerExpansion interface{} 36 | -------------------------------------------------------------------------------- /hack/verify-golint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2014 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | KUBE_ROOT=$(dirname "${BASH_SOURCE}")/.. 22 | 23 | cd "${KUBE_ROOT}" 24 | 25 | PACKAGES=($(go list ./... | sed sXsigs.k8s.io/mcs-apiX..X)) 26 | bad_files=() 27 | for package in "${PACKAGES[@]}"; do 28 | out=$(go -C tools run golang.org/x/lint/golint -min_confidence=0.9 "${package}" 2>&1 | 29 | sed 'sX^../XX;/should not use dot imports/d;/exported const OptionalLabel/d;/^go: downloading/d' ||:) 30 | if [[ -n "${out}" ]]; then 31 | bad_files+=("${out}") 32 | fi 33 | done 34 | if [[ "${#bad_files[@]}" -ne 0 ]]; then 35 | echo "!!! golint problems: " 36 | for err in "${bad_files[@]}"; do 37 | echo "$err" 38 | done 39 | exit 1 40 | fi 41 | 42 | # ex: ts=2 sw=2 et filetype=sh 43 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | rest "k8s.io/client-go/rest" 23 | testing "k8s.io/client-go/testing" 24 | v1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" 25 | ) 26 | 27 | type FakeMulticlusterV1alpha1 struct { 28 | *testing.Fake 29 | } 30 | 31 | func (c *FakeMulticlusterV1alpha1) ServiceExports(namespace string) v1alpha1.ServiceExportInterface { 32 | return newFakeServiceExports(c, namespace) 33 | } 34 | 35 | func (c *FakeMulticlusterV1alpha1) ServiceImports(namespace string) v1alpha1.ServiceImportInterface { 36 | return newFakeServiceImports(c, namespace) 37 | } 38 | 39 | // RESTClient returns a RESTClient that is used to communicate 40 | // with API server by this client implementation. 41 | func (c *FakeMulticlusterV1alpha1) RESTClient() rest.Interface { 42 | var ret *rest.RESTClient 43 | return ret 44 | } 45 | -------------------------------------------------------------------------------- /config/crd-base/multicluster.x-k8s.io_serviceexports.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: apiextensions.k8s.io/v1 15 | kind: CustomResourceDefinition 16 | metadata: 17 | name: serviceexports.multicluster.x-k8s.io 18 | labels: 19 | multicluster.x-k8s.io/release-version: "v0.3.0" 20 | # The revision is updated on each CRD change and reset back to 0 on every new version. 21 | # It can be used together with the version label when installing those CRDs 22 | # and prevent any downgrades. 23 | multicluster.x-k8s.io/crd-schema-revision: "1" 24 | spec: 25 | group: multicluster.x-k8s.io 26 | scope: Namespaced 27 | names: 28 | plural: serviceexports 29 | singular: serviceexport 30 | kind: ServiceExport 31 | shortNames: 32 | - svcex 33 | - svcexport 34 | versions: 35 | - name: v1alpha1 36 | served: true 37 | storage: true 38 | subresources: 39 | status: {} 40 | additionalPrinterColumns: 41 | - name: Age 42 | type: date 43 | jsonPath: .metadata.creationTimestamp 44 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package internalinterfaces 20 | 21 | import ( 22 | time "time" 23 | 24 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | cache "k8s.io/client-go/tools/cache" 27 | versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" 28 | ) 29 | 30 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 31 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 32 | 33 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 34 | type SharedInformerFactory interface { 35 | Start(stopCh <-chan struct{}) 36 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 37 | } 38 | 39 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 40 | type TweakListOptionsFunc func(*v1.ListOptions) 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | 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: 4 | 5 | _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._ 6 | 7 | ## Getting Started 8 | 9 | We have full documentation on how to get started contributing here: 10 | 11 | 14 | 15 | - [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 16 | - [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) 17 | - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers 18 | 19 | ## Mentorship 20 | 21 | - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! 22 | 23 | ## Contact Information 24 | 25 | - [Slack](https://kubernetes.slack.com/messages/sig-service-catalog) 26 | - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-service-catalog) 27 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/apis/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package apis 20 | 21 | import ( 22 | v1alpha1 "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/apis/v1alpha1" 23 | internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" 24 | ) 25 | 26 | // Interface provides access to each of this group's versions. 27 | type Interface interface { 28 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 29 | V1alpha1() v1alpha1.Interface 30 | } 31 | 32 | type group struct { 33 | factory internalinterfaces.SharedInformerFactory 34 | namespace string 35 | tweakListOptions internalinterfaces.TweakListOptionsFunc 36 | } 37 | 38 | // New returns a new Interface. 39 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 40 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 41 | } 42 | 43 | // V1alpha1 returns a new v1alpha1.Interface. 44 | func (g *group) V1alpha1() v1alpha1.Interface { 45 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 46 | } 47 | -------------------------------------------------------------------------------- /config/crd-base/multicluster.x-k8s.io_serviceimports.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: apiextensions.k8s.io/v1 15 | kind: CustomResourceDefinition 16 | metadata: 17 | name: serviceimports.multicluster.x-k8s.io 18 | labels: 19 | multicluster.x-k8s.io/release-version: "v0.3.0" 20 | # The revision is updated on each CRD change and reset back to 0 on every new version. 21 | # It can be used together with the version label when installing those CRDs 22 | # and prevent any downgrades. 23 | multicluster.x-k8s.io/crd-schema-revision: "1" 24 | spec: 25 | group: multicluster.x-k8s.io 26 | scope: Namespaced 27 | names: 28 | plural: serviceimports 29 | singular: serviceimport 30 | kind: ServiceImport 31 | shortNames: 32 | - svcim 33 | - svcimport 34 | versions: 35 | - name: v1alpha1 36 | served: true 37 | storage: true 38 | subresources: 39 | status: {} 40 | additionalPrinterColumns: 41 | - name: Type 42 | type: string 43 | description: The type of this ServiceImport 44 | jsonPath: .spec.type 45 | - name: IP 46 | type: string 47 | description: The VIP for this ServiceImport 48 | jsonPath: .spec.ips 49 | - name: Age 50 | type: date 51 | jsonPath: .metadata.creationTimestamp 52 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kubernetes-sigs/mcs-api/tools 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 7 | k8s.io/code-generator v0.32.5 8 | sigs.k8s.io/controller-tools v0.17.3 9 | ) 10 | 11 | require ( 12 | github.com/fatih/color v1.18.0 // indirect 13 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 14 | github.com/go-logr/logr v1.4.2 // indirect 15 | github.com/gobuffalo/flect v1.0.3 // indirect 16 | github.com/gogo/protobuf v1.3.2 // indirect 17 | github.com/google/gofuzz v1.2.0 // indirect 18 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 19 | github.com/json-iterator/go v1.1.12 // indirect 20 | github.com/mattn/go-colorable v0.1.13 // indirect 21 | github.com/mattn/go-isatty v0.0.20 // indirect 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 23 | github.com/modern-go/reflect2 v1.0.2 // indirect 24 | github.com/spf13/cobra v1.9.1 // indirect 25 | github.com/spf13/pflag v1.0.6 // indirect 26 | github.com/x448/float16 v0.8.4 // indirect 27 | golang.org/x/mod v0.23.0 // indirect 28 | golang.org/x/net v0.35.0 // indirect 29 | golang.org/x/sync v0.11.0 // indirect 30 | golang.org/x/sys v0.30.0 // indirect 31 | golang.org/x/text v0.22.0 // indirect 32 | golang.org/x/tools v0.30.0 // indirect 33 | gopkg.in/inf.v0 v0.9.1 // indirect 34 | gopkg.in/yaml.v2 v2.4.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | k8s.io/api v0.32.2 // indirect 37 | k8s.io/apiextensions-apiserver v0.32.2 // indirect 38 | k8s.io/apimachinery v0.32.5 // indirect 39 | k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect 40 | k8s.io/klog/v2 v2.130.1 // indirect 41 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 42 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 43 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 44 | sigs.k8s.io/yaml v1.4.0 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /conformance/framework.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package conformance 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "time" 23 | 24 | v1 "k8s.io/api/core/v1" 25 | "k8s.io/client-go/kubernetes" 26 | "k8s.io/client-go/kubernetes/scheme" 27 | rest "k8s.io/client-go/rest" 28 | "k8s.io/client-go/tools/remotecommand" 29 | ) 30 | 31 | func execCmd(k8s kubernetes.Interface, config *rest.Config, podName string, podNamespace string, command []string) ([]byte, []byte, error) { 32 | req := k8s.CoreV1().RESTClient().Post().Resource("pods").Name(podName).Namespace(podNamespace).SubResource("exec") 33 | req.VersionedParams(&v1.PodExecOptions{ 34 | Command: command, 35 | Stdin: false, 36 | Stdout: true, 37 | Stderr: true, 38 | TTY: true, 39 | }, scheme.ParameterCodec) 40 | 41 | exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) 42 | if err != nil { 43 | return []byte{}, []byte{}, err 44 | } 45 | 46 | var stdout, stderr bytes.Buffer 47 | 48 | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) 49 | defer cancel() 50 | 51 | err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ 52 | Stdin: nil, 53 | Stdout: &stdout, 54 | Stderr: &stderr, 55 | }) 56 | if err != nil { 57 | return []byte{}, []byte{}, err 58 | } 59 | 60 | return stdout.Bytes(), stderr.Bytes(), nil 61 | } 62 | -------------------------------------------------------------------------------- /hack/verify-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2014 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. 22 | source "${SCRIPT_ROOT}/hack/kube-env.sh" 23 | 24 | SILENT=true 25 | 26 | function is-excluded { 27 | for e in $EXCLUDE; do 28 | if [[ $1 -ef ${BASH_SOURCE} ]]; then 29 | return 30 | fi 31 | if [[ $1 -ef "$SCRIPT_ROOT/hack/$e" ]]; then 32 | return 33 | fi 34 | done 35 | return 1 36 | } 37 | 38 | while getopts ":v" opt; do 39 | case $opt in 40 | v) 41 | SILENT=false 42 | ;; 43 | \?) 44 | echo "Invalid flag: -$OPTARG" >&2 45 | exit 1 46 | ;; 47 | esac 48 | done 49 | 50 | if $SILENT ; then 51 | echo "Running in the silent mode, run with -v if you want to see script logs." 52 | fi 53 | 54 | EXCLUDE="verify-all.sh" 55 | 56 | ret=0 57 | for t in "$SCRIPT_ROOT"/hack/verify-*.sh 58 | do 59 | if is-excluded $t ; then 60 | echo "Skipping $t" 61 | continue 62 | fi 63 | if $SILENT ; then 64 | echo -e "Verifying $t" 65 | if bash "$t" &> /dev/null; then 66 | echo -e "${color_green}SUCCESS${color_norm}" 67 | else 68 | echo -e "${color_red}FAILED${color_norm}" 69 | ret=1 70 | fi 71 | else 72 | bash "$t" || ret=1 73 | fi 74 | done 75 | 76 | exit $ret 77 | 78 | # ex: ts=2 sw=2 et filetype=sh 79 | -------------------------------------------------------------------------------- /hack/kube-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2014 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Some useful colors. 18 | if [[ -z "${color_start-}" ]]; then 19 | declare -r color_start="\033[" 20 | declare -r color_red="${color_start}0;31m" 21 | declare -r color_yellow="${color_start}0;33m" 22 | declare -r color_green="${color_start}0;32m" 23 | declare -r color_norm="${color_start}0m" 24 | fi 25 | 26 | # Returns the server version as MMmmpp, with MM as the major 27 | # component, mm the minor component, and pp as the patch 28 | # revision. e.g. 0.7.1 is echoed as 701, and 1.0.11 would be 29 | # 10011. (This makes for easy integer comparison in bash.) 30 | function kube_server_version() { 31 | local server_version 32 | local major 33 | local minor 34 | local patch 35 | 36 | # This sed expression is the POSIX BRE to match strings like: 37 | # Server Version: &version.Info{Major:"0", Minor:"7+", GitVersion:"v0.7.0-dirty", GitCommit:"ad44234f7152e9c66bc2853575445c7071335e57", GitTreeState:"dirty"} 38 | # and capture the GitVersion portion (which has the patch level) 39 | server_version=$(${KUBECTL} --match-server-version=false version | grep "Server Version:") 40 | read major minor patch < <( 41 | echo ${server_version} | \ 42 | sed "s/.*GitVersion:\"v\([0-9]\{1,\}\)\.\([0-9]\{1,\}\)\.\([0-9]\{1,\}\).*/\1 \2 \3/") 43 | printf "%02d%02d%02d" ${major} ${minor} ${patch} | sed 's/^0*//' 44 | } 45 | -------------------------------------------------------------------------------- /hack/verify-crds.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. 22 | DIFFROOT="${SCRIPT_ROOT}/config" 23 | TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/config" 24 | _tmp="${SCRIPT_ROOT}/_tmp" 25 | # The controller-gen command for generating CRDs from API definitions. 26 | CONTROLLER_GEN="go -C tools run sigs.k8s.io/controller-tools/cmd/controller-gen" 27 | # Need v1 to support defaults in CRDs, unfortunately limiting us to k8s 1.16+ 28 | CRD_OPTIONS="crd:crdVersions=v1" 29 | 30 | cd "${SCRIPT_ROOT}" 31 | 32 | cleanup() { 33 | rm -rf "${_tmp}" 34 | } 35 | trap "cleanup" EXIT SIGINT 36 | 37 | cleanup 38 | 39 | mkdir -p "${TMP_DIFFROOT}" 40 | cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}" 41 | 42 | ${CONTROLLER_GEN} ${CRD_OPTIONS} rbac:roleName=mcs-derived-service-manager webhook \ 43 | paths="${SCRIPT_ROOT}/..." schemapatch:manifests="${SCRIPT_ROOT}/config/crd-base" output:crd:none \ 44 | output:schemapatch:dir="${TMP_DIFFROOT}/crd" output:rbac:dir="${TMP_DIFFROOT}/rbac" 45 | 46 | echo "diffing ${DIFFROOT} against freshly generated codegen in ${TMP_DIFFROOT}" 47 | ret=0 48 | diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? 49 | if [[ $ret -eq 0 ]] 50 | then 51 | echo "${DIFFROOT} up to date." 52 | else 53 | echo "${DIFFROOT} is out of date. Please run 'make manifests'" 54 | exit 1 55 | fi 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-cluster Service APIs 2 | 3 | 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. 4 | 5 | This repo contains the initial implementation according to [KEP-1645][kep] and will 6 | be used for iterative development as we work to meet our Alpha -> Beta 7 | [graduation requirements][grad-reqs]. 8 | 9 | [kep]: https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api 10 | [grad-reqs]: https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#alpha---beta-graduation 11 | 12 | ## Try it out 13 | 14 | To see the API in action, run `make demo` to build and run a local demo against 15 | a pair of kind clusters. Alternatively, you can take a self guided tour. Use: 16 | 17 | - `./scripts/up.sh` to create a pair of clusters with mutually connected networks 18 | and install the `mcs-api-controller`. 19 | 20 | _This will use a pre-existing controller image if available, it's recommended 21 | to run `make docker-build` first._ 22 | - `./demo/demo.sh` to run the same demo as above against your newly created 23 | clusters (must run `./scripts/up.sh` first). 24 | - `./scripts/down.sh` to tear down your clusters. 25 | 26 | ## Community, discussion, contribution, and support 27 | 28 | Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). 29 | 30 | You can reach the maintainers of this project at: 31 | 32 | - [Slack](https://kubernetes.slack.com/messages/sig-multicluster) 33 | - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-multicluster) 34 | 35 | [Our meeting schedule is here]( https://github.com/kubernetes/community/tree/master/sig-multicluster#meetings) 36 | 37 | 38 | ## Technical Leads 39 | 40 | - @pmorie 41 | - @jeremyot 42 | 43 | ### Code of conduct 44 | 45 | Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). 46 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | runtime "k8s.io/apimachinery/pkg/runtime" 24 | schema "k8s.io/apimachinery/pkg/runtime/schema" 25 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 26 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 27 | multiclusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 28 | ) 29 | 30 | var scheme = runtime.NewScheme() 31 | var codecs = serializer.NewCodecFactory(scheme) 32 | 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | multiclusterv1alpha1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/mcs-api 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | k8s.io/api v0.32.5 7 | k8s.io/apimachinery v0.32.5 8 | k8s.io/client-go v0.32.5 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 13 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 14 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 15 | github.com/go-logr/logr v1.4.2 // indirect 16 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 17 | github.com/go-openapi/jsonreference v0.20.2 // indirect 18 | github.com/go-openapi/swag v0.23.0 // indirect 19 | github.com/gogo/protobuf v1.3.2 // indirect 20 | github.com/golang/protobuf v1.5.4 // indirect 21 | github.com/google/gnostic-models v0.6.8 // indirect 22 | github.com/google/go-cmp v0.6.0 // indirect 23 | github.com/google/gofuzz v1.2.0 // indirect 24 | github.com/google/uuid v1.6.0 // indirect 25 | github.com/josharian/intern v1.0.0 // indirect 26 | github.com/json-iterator/go v1.1.12 // indirect 27 | github.com/mailru/easyjson v0.7.7 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 31 | github.com/pkg/errors v0.9.1 // indirect 32 | github.com/x448/float16 v0.8.4 // indirect 33 | golang.org/x/net v0.30.0 // indirect 34 | golang.org/x/oauth2 v0.23.0 // indirect 35 | golang.org/x/sys v0.26.0 // indirect 36 | golang.org/x/term v0.25.0 // indirect 37 | golang.org/x/text v0.19.0 // indirect 38 | golang.org/x/time v0.7.0 // indirect 39 | google.golang.org/protobuf v1.35.1 // indirect 40 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 41 | gopkg.in/inf.v0 v0.9.1 // indirect 42 | gopkg.in/yaml.v3 v3.0.1 // indirect 43 | k8s.io/klog/v2 v2.130.1 // indirect 44 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 45 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 46 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 47 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 48 | sigs.k8s.io/yaml v1.4.0 // indirect 49 | ) 50 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package scheme 20 | 21 | import ( 22 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | runtime "k8s.io/apimachinery/pkg/runtime" 24 | schema "k8s.io/apimachinery/pkg/runtime/schema" 25 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 26 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 27 | multiclusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 28 | ) 29 | 30 | var Scheme = runtime.NewScheme() 31 | var Codecs = serializer.NewCodecFactory(Scheme) 32 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | multiclusterv1alpha1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(Scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/apis/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" 23 | ) 24 | 25 | // Interface provides access to all the informers in this group version. 26 | type Interface interface { 27 | // ServiceExports returns a ServiceExportInformer. 28 | ServiceExports() ServiceExportInformer 29 | // ServiceImports returns a ServiceImportInformer. 30 | ServiceImports() ServiceImportInformer 31 | } 32 | 33 | type version struct { 34 | factory internalinterfaces.SharedInformerFactory 35 | namespace string 36 | tweakListOptions internalinterfaces.TweakListOptionsFunc 37 | } 38 | 39 | // New returns a new Interface. 40 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 41 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 42 | } 43 | 44 | // ServiceExports returns a ServiceExportInformer. 45 | func (v *version) ServiceExports() ServiceExportInformer { 46 | return &serviceExportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 47 | } 48 | 49 | // ServiceImports returns a ServiceImportInformer. 50 | func (v *version) ServiceImports() ServiceImportInformer { 51 | return &serviceImportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 52 | } 53 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_serviceexport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | gentype "k8s.io/client-go/gentype" 23 | v1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 24 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" 25 | ) 26 | 27 | // fakeServiceExports implements ServiceExportInterface 28 | type fakeServiceExports struct { 29 | *gentype.FakeClientWithList[*v1alpha1.ServiceExport, *v1alpha1.ServiceExportList] 30 | Fake *FakeMulticlusterV1alpha1 31 | } 32 | 33 | func newFakeServiceExports(fake *FakeMulticlusterV1alpha1, namespace string) apisv1alpha1.ServiceExportInterface { 34 | return &fakeServiceExports{ 35 | gentype.NewFakeClientWithList[*v1alpha1.ServiceExport, *v1alpha1.ServiceExportList]( 36 | fake.Fake, 37 | namespace, 38 | v1alpha1.SchemeGroupVersion.WithResource("serviceexports"), 39 | v1alpha1.SchemeGroupVersion.WithKind("ServiceExport"), 40 | func() *v1alpha1.ServiceExport { return &v1alpha1.ServiceExport{} }, 41 | func() *v1alpha1.ServiceExportList { return &v1alpha1.ServiceExportList{} }, 42 | func(dst, src *v1alpha1.ServiceExportList) { dst.ListMeta = src.ListMeta }, 43 | func(list *v1alpha1.ServiceExportList) []*v1alpha1.ServiceExport { 44 | return gentype.ToPointerSlice(list.Items) 45 | }, 46 | func(list *v1alpha1.ServiceExportList, items []*v1alpha1.ServiceExport) { 47 | list.Items = gentype.FromPointerSlice(items) 48 | }, 49 | ), 50 | fake, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_serviceimport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | gentype "k8s.io/client-go/gentype" 23 | v1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 24 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" 25 | ) 26 | 27 | // fakeServiceImports implements ServiceImportInterface 28 | type fakeServiceImports struct { 29 | *gentype.FakeClientWithList[*v1alpha1.ServiceImport, *v1alpha1.ServiceImportList] 30 | Fake *FakeMulticlusterV1alpha1 31 | } 32 | 33 | func newFakeServiceImports(fake *FakeMulticlusterV1alpha1, namespace string) apisv1alpha1.ServiceImportInterface { 34 | return &fakeServiceImports{ 35 | gentype.NewFakeClientWithList[*v1alpha1.ServiceImport, *v1alpha1.ServiceImportList]( 36 | fake.Fake, 37 | namespace, 38 | v1alpha1.SchemeGroupVersion.WithResource("serviceimports"), 39 | v1alpha1.SchemeGroupVersion.WithKind("ServiceImport"), 40 | func() *v1alpha1.ServiceImport { return &v1alpha1.ServiceImport{} }, 41 | func() *v1alpha1.ServiceImportList { return &v1alpha1.ServiceImportList{} }, 42 | func(dst, src *v1alpha1.ServiceImportList) { dst.ListMeta = src.ListMeta }, 43 | func(list *v1alpha1.ServiceImportList) []*v1alpha1.ServiceImport { 44 | return gentype.ToPointerSlice(list.Items) 45 | }, 46 | func(list *v1alpha1.ServiceImportList, items []*v1alpha1.ServiceImport) { 47 | list.Items = gentype.FromPointerSlice(items) 48 | }, 49 | ), 50 | fake, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /controllers/cmd/servicecontroller/servicecontroller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | "k8s.io/apimachinery/pkg/runtime" 24 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 25 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 28 | "sigs.k8s.io/controller-runtime/pkg/metrics/server" 29 | "sigs.k8s.io/controller-runtime/pkg/webhook" 30 | "sigs.k8s.io/mcs-api/controllers" 31 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 32 | ) 33 | 34 | var ( 35 | scheme = runtime.NewScheme() 36 | setupLog = ctrl.Log.WithName("setup") 37 | ) 38 | 39 | func init() { 40 | clientgoscheme.AddToScheme(scheme) 41 | v1alpha1.AddToScheme(scheme) 42 | } 43 | 44 | func main() { 45 | var metricsAddr string 46 | var enableLeaderElection bool 47 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 48 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 49 | "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") 50 | flag.Parse() 51 | opts := ctrl.Options{ 52 | Scheme: scheme, 53 | Metrics: server.Options{ 54 | BindAddress: metricsAddr, 55 | }, 56 | LeaderElection: enableLeaderElection, 57 | WebhookServer: webhook.NewServer(webhook.Options{ 58 | Port: 9443, 59 | }), 60 | } 61 | ctrl.SetLogger(zap.New(zap.UseDevMode(true))) 62 | 63 | if err := controllers.Start(ctrl.SetupSignalHandler(), ctrl.GetConfigOrDie(), setupLog, opts); err != nil { 64 | setupLog.Error(err, "problem running controllers") 65 | os.Exit(1) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /demo/udemo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2016 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | readonly reset=$(tput sgr0) 17 | readonly green=$(tput bold; tput setaf 2) 18 | readonly yellow=$(tput bold; tput setaf 3) 19 | readonly blue=$(tput bold; tput setaf 6) 20 | readonly timeout=$(if [ "$(uname)" == "Darwin" ]; then echo "1"; else echo "0.1"; fi) 21 | 22 | function desc() { 23 | maybe_first_prompt 24 | echo "$blue# $@$reset" 25 | prompt 26 | } 27 | 28 | function prompt() { 29 | echo -n "$yellow\$ $reset" 30 | } 31 | 32 | started="" 33 | function maybe_first_prompt() { 34 | if [ -z "$started" ]; then 35 | prompt 36 | started=true 37 | fi 38 | } 39 | 40 | # After a `run` this variable will hold the stdout of the command that was run. 41 | # If the command was interactive, this will likely be garbage. 42 | DEMO_RUN_STDOUT="" 43 | 44 | function run() { 45 | maybe_first_prompt 46 | rate=25 47 | if [ -n "$DEMO_RUN_FAST" ]; then 48 | rate=1000 49 | fi 50 | echo "$green$1$reset" | pv -qL $rate 51 | if [ -n "$DEMO_RUN_FAST" ]; then 52 | sleep 0.5 53 | fi 54 | OFILE="$(mktemp -t $(basename $0).XXXXXX)" 55 | if [[ "$OSTYPE" == "darwin"* ]]; then 56 | script -q -a "$OFILE" bash -c "$1" 57 | else 58 | script -eq -c "$1" -f "$OFILE" 59 | fi 60 | r=$? 61 | read -d '' -t "${timeout}" -n 10000 # clear stdin 62 | prompt 63 | if [ -z "$DEMO_AUTO_RUN" ]; then 64 | read -s 65 | fi 66 | DEMO_RUN_STDOUT="$(tail -n +2 $OFILE | sed 's/\r//g')" 67 | return $r 68 | } 69 | 70 | function relative() { 71 | for arg; do 72 | echo "$(realpath $(dirname $(which $0)))/$arg" | sed "s|$(realpath $(pwd))|.|" 73 | done 74 | } 75 | 76 | SSH_NODE=$(kubectl get nodes | tail -1 | cut -f1 -d' ') 77 | 78 | trap "echo" EXIT 79 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package externalversions 20 | 21 | import ( 22 | fmt "fmt" 23 | 24 | schema "k8s.io/apimachinery/pkg/runtime/schema" 25 | cache "k8s.io/client-go/tools/cache" 26 | v1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 27 | ) 28 | 29 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 30 | // sharedInformers based on type 31 | type GenericInformer interface { 32 | Informer() cache.SharedIndexInformer 33 | Lister() cache.GenericLister 34 | } 35 | 36 | type genericInformer struct { 37 | informer cache.SharedIndexInformer 38 | resource schema.GroupResource 39 | } 40 | 41 | // Informer returns the SharedIndexInformer. 42 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 43 | return f.informer 44 | } 45 | 46 | // Lister returns the GenericLister. 47 | func (f *genericInformer) Lister() cache.GenericLister { 48 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 49 | } 50 | 51 | // ForResource gives generic access to a shared informer of the matching type 52 | // TODO extend this to unknown resources with a client pool 53 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 54 | switch resource { 55 | // Group=multicluster.x-k8s.io, Version=v1alpha1 56 | case v1alpha1.SchemeGroupVersion.WithResource("serviceexports"): 57 | return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1alpha1().ServiceExports().Informer()}, nil 58 | case v1alpha1.SchemeGroupVersion.WithResource("serviceimports"): 59 | return &genericInformer{resource: resource.GroupResource(), informer: f.Multicluster().V1alpha1().ServiceImports().Informer()}, nil 60 | 61 | } 62 | 63 | return nil, fmt.Errorf("no informer found for %v", resource) 64 | } 65 | -------------------------------------------------------------------------------- /conformance/go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/mcs-api/conformance 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/onsi/ginkgo/v2 v2.21.0 7 | github.com/onsi/gomega v1.35.1 8 | k8s.io/api v0.32.5 9 | k8s.io/apimachinery v0.32.5 10 | k8s.io/client-go v0.32.5 11 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 12 | sigs.k8s.io/mcs-api v0.3.0 13 | ) 14 | 15 | replace sigs.k8s.io/mcs-api => .. 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 19 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 20 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 21 | github.com/go-logr/logr v1.4.2 // indirect 22 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 23 | github.com/go-openapi/jsonreference v0.20.2 // indirect 24 | github.com/go-openapi/swag v0.23.0 // indirect 25 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 26 | github.com/gogo/protobuf v1.3.2 // indirect 27 | github.com/golang/protobuf v1.5.4 // indirect 28 | github.com/google/gnostic-models v0.6.8 // indirect 29 | github.com/google/go-cmp v0.6.0 // indirect 30 | github.com/google/gofuzz v1.2.0 // indirect 31 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect 32 | github.com/google/uuid v1.6.0 // indirect 33 | github.com/gorilla/websocket v1.5.0 // indirect 34 | github.com/josharian/intern v1.0.0 // indirect 35 | github.com/json-iterator/go v1.1.12 // indirect 36 | github.com/mailru/easyjson v0.7.7 // indirect 37 | github.com/moby/spdystream v0.5.0 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 41 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 42 | github.com/pkg/errors v0.9.1 // indirect 43 | github.com/spf13/pflag v1.0.5 // indirect 44 | github.com/x448/float16 v0.8.4 // indirect 45 | golang.org/x/net v0.30.0 // indirect 46 | golang.org/x/oauth2 v0.23.0 // indirect 47 | golang.org/x/sys v0.26.0 // indirect 48 | golang.org/x/term v0.25.0 // indirect 49 | golang.org/x/text v0.19.0 // indirect 50 | golang.org/x/time v0.7.0 // indirect 51 | golang.org/x/tools v0.26.0 // indirect 52 | google.golang.org/protobuf v1.35.1 // indirect 53 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 54 | gopkg.in/inf.v0 v0.9.1 // indirect 55 | gopkg.in/yaml.v3 v3.0.1 // indirect 56 | k8s.io/klog/v2 v2.130.1 // indirect 57 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 58 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 59 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 60 | sigs.k8s.io/yaml v1.4.0 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /e2e/go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/mcs-api/e2e 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/onsi/ginkgo/v2 v2.21.0 7 | github.com/onsi/gomega v1.35.1 8 | k8s.io/api v0.32.5 9 | k8s.io/apimachinery v0.32.5 10 | k8s.io/client-go v0.32.5 11 | sigs.k8s.io/mcs-api v0.3.0 12 | ) 13 | 14 | replace sigs.k8s.io/mcs-api => .. 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 18 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 19 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 20 | github.com/go-logr/logr v1.4.2 // indirect 21 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 22 | github.com/go-openapi/jsonreference v0.20.2 // indirect 23 | github.com/go-openapi/swag v0.23.0 // indirect 24 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/golang/protobuf v1.5.4 // indirect 27 | github.com/google/gnostic-models v0.6.8 // indirect 28 | github.com/google/go-cmp v0.6.0 // indirect 29 | github.com/google/gofuzz v1.2.0 // indirect 30 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect 31 | github.com/google/uuid v1.6.0 // indirect 32 | github.com/gorilla/websocket v1.5.0 // indirect 33 | github.com/josharian/intern v1.0.0 // indirect 34 | github.com/json-iterator/go v1.1.12 // indirect 35 | github.com/mailru/easyjson v0.7.7 // indirect 36 | github.com/moby/spdystream v0.5.0 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v1.0.2 // indirect 39 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 40 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 41 | github.com/pkg/errors v0.9.1 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | github.com/x448/float16 v0.8.4 // indirect 44 | golang.org/x/net v0.30.0 // indirect 45 | golang.org/x/oauth2 v0.23.0 // indirect 46 | golang.org/x/sys v0.26.0 // indirect 47 | golang.org/x/term v0.25.0 // indirect 48 | golang.org/x/text v0.19.0 // indirect 49 | golang.org/x/time v0.7.0 // indirect 50 | golang.org/x/tools v0.26.0 // indirect 51 | google.golang.org/protobuf v1.35.1 // indirect 52 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 53 | gopkg.in/inf.v0 v0.9.1 // indirect 54 | gopkg.in/yaml.v3 v3.0.1 // indirect 55 | k8s.io/klog/v2 v2.130.1 // indirect 56 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 57 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 58 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 59 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 60 | sigs.k8s.io/yaml v1.4.0 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /pkg/apis/v1alpha1/zz_generated.register.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2020 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by register-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | schema "k8s.io/apimachinery/pkg/runtime/schema" 28 | ) 29 | 30 | // GroupName specifies the group name used to register the objects. 31 | const GroupName = "multicluster.x-k8s.io" 32 | 33 | // GroupVersion specifies the group and the version used to register the objects. 34 | var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1alpha1"} 35 | 36 | // SchemeGroupVersion is group version used to register these objects 37 | // Deprecated: use GroupVersion instead. 38 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} 39 | 40 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 41 | func Resource(resource string) schema.GroupResource { 42 | return SchemeGroupVersion.WithResource(resource).GroupResource() 43 | } 44 | 45 | var ( 46 | // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. 47 | SchemeBuilder runtime.SchemeBuilder 48 | localSchemeBuilder = &SchemeBuilder 49 | // Deprecated: use Install instead 50 | AddToScheme = localSchemeBuilder.AddToScheme 51 | Install = localSchemeBuilder.AddToScheme 52 | ) 53 | 54 | func init() { 55 | // We only register manually written functions here. The registration of the 56 | // generated functions takes place in the generated files. The separation 57 | // makes the code compile even when the generated files are missing. 58 | localSchemeBuilder.Register(addKnownTypes) 59 | } 60 | 61 | // Adds the list of known types to Scheme. 62 | func addKnownTypes(scheme *runtime.Scheme) error { 63 | scheme.AddKnownTypes(SchemeGroupVersion, 64 | &ServiceExport{}, 65 | &ServiceExportList{}, 66 | &ServiceImport{}, 67 | &ServiceImportList{}, 68 | ) 69 | // AddToGroupVersion allows the serialization of client types like ListOptions. 70 | v1.AddToGroupVersion(scheme, SchemeGroupVersion) 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /hack/update-codegen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. 22 | 23 | go -C tools install k8s.io/code-generator/cmd/{client-gen,lister-gen,informer-gen,deepcopy-gen,register-gen} 24 | 25 | # Go installs the above commands to get installed in $GOBIN if defined, and $GOPATH/bin otherwise: 26 | GOBIN="$(go env GOBIN)" 27 | gobin="${GOBIN:-$(go env GOPATH)/bin}" 28 | 29 | OUTPUT_PKG=sigs.k8s.io/mcs-api/pkg/client 30 | OUTPUT_DIR=$SCRIPT_ROOT/pkg/client 31 | FQ_APIS=sigs.k8s.io/mcs-api/pkg/apis/v1alpha1 32 | CLIENTSET_NAME=versioned 33 | CLIENTSET_PKG_NAME=clientset 34 | 35 | if [[ "${VERIFY_CODEGEN:-}" == "true" ]]; then 36 | echo "Running in verification mode" 37 | ORIG_OUTPUT_DIR="$OUTPUT_DIR" 38 | OUTPUT_DIR=$(mktemp -d) 39 | trap "rm -rf $OUTPUT_DIR" EXIT 40 | else 41 | # Clear existing code before re-generating it 42 | rm -rf "$OUTPUT_DIR" 43 | fi 44 | COMMON_FLAGS="--go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt" 45 | 46 | echo "Generating clientset at ${OUTPUT_PKG}/${CLIENTSET_PKG_NAME}" 47 | "${gobin}/client-gen" --clientset-name "${CLIENTSET_NAME}" --input-base "" --input "${FQ_APIS}" --output-pkg "${OUTPUT_PKG}/${CLIENTSET_PKG_NAME}" --output-dir "$OUTPUT_DIR/$CLIENTSET_PKG_NAME" ${COMMON_FLAGS} 48 | 49 | echo "Generating listers at ${OUTPUT_PKG}/listers" 50 | "${gobin}/lister-gen" "${FQ_APIS}" --output-pkg "${OUTPUT_PKG}/listers" --output-dir "${OUTPUT_DIR}/listers" ${COMMON_FLAGS} 51 | 52 | echo "Generating informers at ${OUTPUT_PKG}/informers" 53 | "${gobin}/informer-gen" \ 54 | "${FQ_APIS}" \ 55 | --versioned-clientset-package "${OUTPUT_PKG}/${CLIENTSET_PKG_NAME}/${CLIENTSET_NAME}" \ 56 | --listers-package "${OUTPUT_PKG}/listers" \ 57 | --output-pkg "${OUTPUT_PKG}/informers" \ 58 | --output-dir "${OUTPUT_DIR}/informers" \ 59 | ${COMMON_FLAGS} 60 | 61 | echo "Generating register at ${FQ_APIS}" 62 | "${gobin}/register-gen" ${FQ_APIS} --output-file zz_generated.register.go ${COMMON_FLAGS} 63 | 64 | if [[ "${VERIFY_CODEGEN:-}" == "true" ]]; then 65 | diff -urN "$ORIG_OUTPUT_DIR" "$OUTPUT_DIR" 66 | fi 67 | -------------------------------------------------------------------------------- /controllers/endpointslice.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/go-logr/logr" 23 | discoveryv1 "k8s.io/api/discovery/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | ctrl "sigs.k8s.io/controller-runtime" 26 | "sigs.k8s.io/controller-runtime/pkg/client" 27 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 28 | ) 29 | 30 | // EndpointSliceReconciler reconciles a EndpointSlice object 31 | type EndpointSliceReconciler struct { 32 | client.Client 33 | Log logr.Logger 34 | } 35 | 36 | // +kubebuilder:rbac:groups=discovery.k8s.io,resources=endpointslices,verbs=get;list;watch;update;patch 37 | 38 | func shouldIgnoreEndpointSlice(epSlice *discoveryv1.EndpointSlice) bool { 39 | if epSlice.DeletionTimestamp != nil { 40 | return true 41 | } 42 | if epSlice.Labels[v1alpha1.LabelServiceName] == "" { 43 | return true 44 | } 45 | return false 46 | } 47 | 48 | // Reconcile the changes. 49 | func (r *EndpointSliceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 50 | log := r.Log.WithValues("endpointslice", req.NamespacedName) 51 | 52 | var epSlice discoveryv1.EndpointSlice 53 | if err := r.Client.Get(ctx, req.NamespacedName, &epSlice); err != nil { 54 | return ctrl.Result{}, client.IgnoreNotFound(err) 55 | } 56 | if shouldIgnoreEndpointSlice(&epSlice) { 57 | return ctrl.Result{}, nil 58 | } 59 | // Ensure the EndpointSlice is labelled to match the ServiceImport's derived 60 | // Service. 61 | serviceName := derivedName(types.NamespacedName{Namespace: epSlice.Namespace, Name: epSlice.Labels[v1alpha1.LabelServiceName]}) 62 | if epSlice.Labels[discoveryv1.LabelServiceName] == serviceName { 63 | return ctrl.Result{}, nil 64 | } 65 | epSlice.Labels[discoveryv1.LabelServiceName] = serviceName 66 | if err := r.Client.Update(ctx, &epSlice); err != nil { 67 | return ctrl.Result{}, err 68 | } 69 | log.Info("added label", discoveryv1.LabelServiceName, serviceName) 70 | return ctrl.Result{}, nil 71 | } 72 | 73 | // SetupWithManager wires up the controller. 74 | func (r *EndpointSliceReconciler) SetupWithManager(mgr ctrl.Manager) error { 75 | return ctrl.NewControllerManagedBy(mgr).For(&discoveryv1.EndpointSlice{}).Complete(r) 76 | } 77 | -------------------------------------------------------------------------------- /controllers/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "crypto/sha256" 22 | "encoding/base32" 23 | "os" 24 | "strings" 25 | 26 | "github.com/go-logr/logr" 27 | "k8s.io/apimachinery/pkg/types" 28 | "k8s.io/client-go/rest" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | ) 31 | 32 | const ( 33 | // DerivedServiceAnnotation is set on a ServiceImport to reference the 34 | // derived Service that represents the imported service for kube-proxy. 35 | DerivedServiceAnnotation = "multicluster.kubernetes.io/derived-service" 36 | serviceImportKind = "ServiceImport" 37 | ) 38 | 39 | func derivedName(name types.NamespacedName) string { 40 | hash := sha256.New() 41 | hash.Write([]byte(name.String())) 42 | return "derived-" + strings.ToLower(base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(hash.Sum(nil)))[:10] 43 | } 44 | 45 | // Start the controllers with the supplied config 46 | func Start(ctx context.Context, cfg *rest.Config, setupLog logr.Logger, opts ctrl.Options) error { 47 | mgr, err := ctrl.NewManager(cfg, opts) 48 | if err != nil { 49 | setupLog.Error(err, "unable to start manager") 50 | os.Exit(1) 51 | } 52 | 53 | if err = (&ServiceImportReconciler{ 54 | Client: mgr.GetClient(), 55 | Log: ctrl.Log.WithName("controllers").WithName("ServiceImport"), 56 | }).SetupWithManager(mgr); err != nil { 57 | setupLog.Error(err, "unable to create controller", "controller", "ServiceImport") 58 | return err 59 | } 60 | if err = (&ServiceReconciler{ 61 | Client: mgr.GetClient(), 62 | Log: ctrl.Log.WithName("controllers").WithName("Service"), 63 | }).SetupWithManager(mgr); err != nil { 64 | setupLog.Error(err, "unable to create controller", "controller", "Service") 65 | return err 66 | } 67 | if err = (&EndpointSliceReconciler{ 68 | Client: mgr.GetClient(), 69 | Log: ctrl.Log.WithName("controllers").WithName("EndpointSlice"), 70 | }).SetupWithManager(mgr); err != nil { 71 | setupLog.Error(err, "unable to create controller", "controller", "EndpointSlice") 72 | return err 73 | } 74 | 75 | setupLog.Info("starting manager") 76 | if err := mgr.Start(ctx); err != nil { 77 | setupLog.Error(err, "problem running manager") 78 | return err 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /controllers/service.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "slices" 22 | 23 | "github.com/go-logr/logr" 24 | v1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/types" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 30 | ) 31 | 32 | // ServiceReconciler reconciles a Service object 33 | type ServiceReconciler struct { 34 | client.Client 35 | Log logr.Logger 36 | } 37 | 38 | // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch 39 | 40 | func serviceImportOwner(refs []metav1.OwnerReference) string { 41 | for _, ref := range refs { 42 | if ref.APIVersion == v1alpha1.GroupVersion.String() && ref.Kind == serviceImportKind { 43 | return ref.Name 44 | } 45 | } 46 | return "" 47 | } 48 | 49 | // Reconcile the changes. 50 | func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 51 | log := r.Log.WithValues("service", req.NamespacedName) 52 | var service v1.Service 53 | if err := r.Client.Get(ctx, req.NamespacedName, &service); err != nil { 54 | return ctrl.Result{}, client.IgnoreNotFound(err) 55 | } 56 | if service.DeletionTimestamp != nil { 57 | return ctrl.Result{}, nil 58 | } 59 | importName := serviceImportOwner(service.OwnerReferences) 60 | if importName == "" { 61 | return ctrl.Result{}, nil 62 | } 63 | var svcImport v1alpha1.ServiceImport 64 | if err := r.Client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: importName}, &svcImport); err != nil { 65 | return ctrl.Result{}, err 66 | } 67 | 68 | ipsLen := min(2, len(service.Spec.ClusterIPs)) 69 | desiredIPs := service.Spec.ClusterIPs[:ipsLen] 70 | if service.Spec.ClusterIP == v1.ClusterIPNone { 71 | desiredIPs = []string{} 72 | } 73 | if slices.Equal(desiredIPs, svcImport.Spec.IPs) { 74 | return ctrl.Result{}, nil 75 | } 76 | 77 | svcImport.Spec.IPs = desiredIPs 78 | if err := r.Client.Update(ctx, &svcImport); err != nil { 79 | return ctrl.Result{}, err 80 | } 81 | log.Info("updated serviceimport ip", "ip", service.Spec.ClusterIP) 82 | return ctrl.Result{}, nil 83 | } 84 | 85 | // SetupWithManager wires up the controller. 86 | func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { 87 | return ctrl.NewControllerManagedBy(mgr).For(&v1.Service{}).Complete(r) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/client/listers/apis/v1alpha1/serviceexport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | labels "k8s.io/apimachinery/pkg/labels" 23 | listers "k8s.io/client-go/listers" 24 | cache "k8s.io/client-go/tools/cache" 25 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 26 | ) 27 | 28 | // ServiceExportLister helps list ServiceExports. 29 | // All objects returned here must be treated as read-only. 30 | type ServiceExportLister interface { 31 | // List lists all ServiceExports in the indexer. 32 | // Objects returned here must be treated as read-only. 33 | List(selector labels.Selector) (ret []*apisv1alpha1.ServiceExport, err error) 34 | // ServiceExports returns an object that can list and get ServiceExports. 35 | ServiceExports(namespace string) ServiceExportNamespaceLister 36 | ServiceExportListerExpansion 37 | } 38 | 39 | // serviceExportLister implements the ServiceExportLister interface. 40 | type serviceExportLister struct { 41 | listers.ResourceIndexer[*apisv1alpha1.ServiceExport] 42 | } 43 | 44 | // NewServiceExportLister returns a new ServiceExportLister. 45 | func NewServiceExportLister(indexer cache.Indexer) ServiceExportLister { 46 | return &serviceExportLister{listers.New[*apisv1alpha1.ServiceExport](indexer, apisv1alpha1.Resource("serviceexport"))} 47 | } 48 | 49 | // ServiceExports returns an object that can list and get ServiceExports. 50 | func (s *serviceExportLister) ServiceExports(namespace string) ServiceExportNamespaceLister { 51 | return serviceExportNamespaceLister{listers.NewNamespaced[*apisv1alpha1.ServiceExport](s.ResourceIndexer, namespace)} 52 | } 53 | 54 | // ServiceExportNamespaceLister helps list and get ServiceExports. 55 | // All objects returned here must be treated as read-only. 56 | type ServiceExportNamespaceLister interface { 57 | // List lists all ServiceExports in the indexer for a given namespace. 58 | // Objects returned here must be treated as read-only. 59 | List(selector labels.Selector) (ret []*apisv1alpha1.ServiceExport, err error) 60 | // Get retrieves the ServiceExport from the indexer for a given namespace and name. 61 | // Objects returned here must be treated as read-only. 62 | Get(name string) (*apisv1alpha1.ServiceExport, error) 63 | ServiceExportNamespaceListerExpansion 64 | } 65 | 66 | // serviceExportNamespaceLister implements the ServiceExportNamespaceLister 67 | // interface. 68 | type serviceExportNamespaceLister struct { 69 | listers.ResourceIndexer[*apisv1alpha1.ServiceExport] 70 | } 71 | -------------------------------------------------------------------------------- /pkg/client/listers/apis/v1alpha1/serviceimport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | labels "k8s.io/apimachinery/pkg/labels" 23 | listers "k8s.io/client-go/listers" 24 | cache "k8s.io/client-go/tools/cache" 25 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 26 | ) 27 | 28 | // ServiceImportLister helps list ServiceImports. 29 | // All objects returned here must be treated as read-only. 30 | type ServiceImportLister interface { 31 | // List lists all ServiceImports in the indexer. 32 | // Objects returned here must be treated as read-only. 33 | List(selector labels.Selector) (ret []*apisv1alpha1.ServiceImport, err error) 34 | // ServiceImports returns an object that can list and get ServiceImports. 35 | ServiceImports(namespace string) ServiceImportNamespaceLister 36 | ServiceImportListerExpansion 37 | } 38 | 39 | // serviceImportLister implements the ServiceImportLister interface. 40 | type serviceImportLister struct { 41 | listers.ResourceIndexer[*apisv1alpha1.ServiceImport] 42 | } 43 | 44 | // NewServiceImportLister returns a new ServiceImportLister. 45 | func NewServiceImportLister(indexer cache.Indexer) ServiceImportLister { 46 | return &serviceImportLister{listers.New[*apisv1alpha1.ServiceImport](indexer, apisv1alpha1.Resource("serviceimport"))} 47 | } 48 | 49 | // ServiceImports returns an object that can list and get ServiceImports. 50 | func (s *serviceImportLister) ServiceImports(namespace string) ServiceImportNamespaceLister { 51 | return serviceImportNamespaceLister{listers.NewNamespaced[*apisv1alpha1.ServiceImport](s.ResourceIndexer, namespace)} 52 | } 53 | 54 | // ServiceImportNamespaceLister helps list and get ServiceImports. 55 | // All objects returned here must be treated as read-only. 56 | type ServiceImportNamespaceLister interface { 57 | // List lists all ServiceImports in the indexer for a given namespace. 58 | // Objects returned here must be treated as read-only. 59 | List(selector labels.Selector) (ret []*apisv1alpha1.ServiceImport, err error) 60 | // Get retrieves the ServiceImport from the indexer for a given namespace and name. 61 | // Objects returned here must be treated as read-only. 62 | Get(name string) (*apisv1alpha1.ServiceImport, error) 63 | ServiceImportNamespaceListerExpansion 64 | } 65 | 66 | // serviceImportNamespaceLister implements the ServiceImportNamespaceLister 67 | // interface. 68 | type serviceImportNamespaceLister struct { 69 | listers.ResourceIndexer[*apisv1alpha1.ServiceImport] 70 | } 71 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/serviceexport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | context "context" 23 | 24 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | types "k8s.io/apimachinery/pkg/types" 26 | watch "k8s.io/apimachinery/pkg/watch" 27 | gentype "k8s.io/client-go/gentype" 28 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 29 | scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" 30 | ) 31 | 32 | // ServiceExportsGetter has a method to return a ServiceExportInterface. 33 | // A group's client should implement this interface. 34 | type ServiceExportsGetter interface { 35 | ServiceExports(namespace string) ServiceExportInterface 36 | } 37 | 38 | // ServiceExportInterface has methods to work with ServiceExport resources. 39 | type ServiceExportInterface interface { 40 | Create(ctx context.Context, serviceExport *apisv1alpha1.ServiceExport, opts v1.CreateOptions) (*apisv1alpha1.ServiceExport, error) 41 | Update(ctx context.Context, serviceExport *apisv1alpha1.ServiceExport, opts v1.UpdateOptions) (*apisv1alpha1.ServiceExport, error) 42 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 43 | UpdateStatus(ctx context.Context, serviceExport *apisv1alpha1.ServiceExport, opts v1.UpdateOptions) (*apisv1alpha1.ServiceExport, error) 44 | Delete(ctx context.Context, name string, opts v1.DeleteOptions) error 45 | DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error 46 | Get(ctx context.Context, name string, opts v1.GetOptions) (*apisv1alpha1.ServiceExport, error) 47 | List(ctx context.Context, opts v1.ListOptions) (*apisv1alpha1.ServiceExportList, error) 48 | Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) 49 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apisv1alpha1.ServiceExport, err error) 50 | ServiceExportExpansion 51 | } 52 | 53 | // serviceExports implements ServiceExportInterface 54 | type serviceExports struct { 55 | *gentype.ClientWithList[*apisv1alpha1.ServiceExport, *apisv1alpha1.ServiceExportList] 56 | } 57 | 58 | // newServiceExports returns a ServiceExports 59 | func newServiceExports(c *MulticlusterV1alpha1Client, namespace string) *serviceExports { 60 | return &serviceExports{ 61 | gentype.NewClientWithList[*apisv1alpha1.ServiceExport, *apisv1alpha1.ServiceExportList]( 62 | "serviceexports", 63 | c.RESTClient(), 64 | scheme.ParameterCodec, 65 | namespace, 66 | func() *apisv1alpha1.ServiceExport { return &apisv1alpha1.ServiceExport{} }, 67 | func() *apisv1alpha1.ServiceExportList { return &apisv1alpha1.ServiceExportList{} }, 68 | ), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/serviceimport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | context "context" 23 | 24 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | types "k8s.io/apimachinery/pkg/types" 26 | watch "k8s.io/apimachinery/pkg/watch" 27 | gentype "k8s.io/client-go/gentype" 28 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 29 | scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" 30 | ) 31 | 32 | // ServiceImportsGetter has a method to return a ServiceImportInterface. 33 | // A group's client should implement this interface. 34 | type ServiceImportsGetter interface { 35 | ServiceImports(namespace string) ServiceImportInterface 36 | } 37 | 38 | // ServiceImportInterface has methods to work with ServiceImport resources. 39 | type ServiceImportInterface interface { 40 | Create(ctx context.Context, serviceImport *apisv1alpha1.ServiceImport, opts v1.CreateOptions) (*apisv1alpha1.ServiceImport, error) 41 | Update(ctx context.Context, serviceImport *apisv1alpha1.ServiceImport, opts v1.UpdateOptions) (*apisv1alpha1.ServiceImport, error) 42 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 43 | UpdateStatus(ctx context.Context, serviceImport *apisv1alpha1.ServiceImport, opts v1.UpdateOptions) (*apisv1alpha1.ServiceImport, error) 44 | Delete(ctx context.Context, name string, opts v1.DeleteOptions) error 45 | DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error 46 | Get(ctx context.Context, name string, opts v1.GetOptions) (*apisv1alpha1.ServiceImport, error) 47 | List(ctx context.Context, opts v1.ListOptions) (*apisv1alpha1.ServiceImportList, error) 48 | Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) 49 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apisv1alpha1.ServiceImport, err error) 50 | ServiceImportExpansion 51 | } 52 | 53 | // serviceImports implements ServiceImportInterface 54 | type serviceImports struct { 55 | *gentype.ClientWithList[*apisv1alpha1.ServiceImport, *apisv1alpha1.ServiceImportList] 56 | } 57 | 58 | // newServiceImports returns a ServiceImports 59 | func newServiceImports(c *MulticlusterV1alpha1Client, namespace string) *serviceImports { 60 | return &serviceImports{ 61 | gentype.NewClientWithList[*apisv1alpha1.ServiceImport, *apisv1alpha1.ServiceImportList]( 62 | "serviceimports", 63 | c.RESTClient(), 64 | scheme.ParameterCodec, 65 | namespace, 66 | func() *apisv1alpha1.ServiceImport { return &apisv1alpha1.ServiceImport{} }, 67 | func() *apisv1alpha1.ServiceImportList { return &apisv1alpha1.ServiceImportList{} }, 68 | ), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/watch" 24 | "k8s.io/client-go/discovery" 25 | fakediscovery "k8s.io/client-go/discovery/fake" 26 | "k8s.io/client-go/testing" 27 | clientset "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" 28 | multiclusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" 29 | fakemulticlusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1/fake" 30 | ) 31 | 32 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 33 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 34 | // without applying any field management, validations and/or defaults. It shouldn't be considered a replacement 35 | // for a real clientset and is mostly useful in simple unit tests. 36 | // 37 | // DEPRECATED: NewClientset replaces this with support for field management, which significantly improves 38 | // server side apply testing. NewClientset is only available when apply configurations are generated (e.g. 39 | // via --with-applyconfig). 40 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 41 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 42 | for _, obj := range objects { 43 | if err := o.Add(obj); err != nil { 44 | panic(err) 45 | } 46 | } 47 | 48 | cs := &Clientset{tracker: o} 49 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 50 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 51 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 52 | gvr := action.GetResource() 53 | ns := action.GetNamespace() 54 | watch, err := o.Watch(gvr, ns) 55 | if err != nil { 56 | return false, nil, err 57 | } 58 | return true, watch, nil 59 | }) 60 | 61 | return cs 62 | } 63 | 64 | // Clientset implements clientset.Interface. Meant to be embedded into a 65 | // struct to get a default implementation. This makes faking out just the method 66 | // you want to test easier. 67 | type Clientset struct { 68 | testing.Fake 69 | discovery *fakediscovery.FakeDiscovery 70 | tracker testing.ObjectTracker 71 | } 72 | 73 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 74 | return c.discovery 75 | } 76 | 77 | func (c *Clientset) Tracker() testing.ObjectTracker { 78 | return c.tracker 79 | } 80 | 81 | var ( 82 | _ clientset.Interface = &Clientset{} 83 | _ testing.FakeClient = &Clientset{} 84 | ) 85 | 86 | // MulticlusterV1alpha1 retrieves the MulticlusterV1alpha1Client 87 | func (c *Clientset) MulticlusterV1alpha1() multiclusterv1alpha1.MulticlusterV1alpha1Interface { 88 | return &fakemulticlusterv1alpha1.FakeMulticlusterV1alpha1{Fake: &c.Fake} 89 | } 90 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | DOCKER ?= docker 16 | # TOP is the current directory where this Makefile lives. 17 | TOP := $(dir $(firstword $(MAKEFILE_LIST))) 18 | # ROOT is the root of the mkdocs tree. 19 | ROOT := $(abspath $(TOP)) 20 | # Image URL to use all building/pushing image targets 21 | IMG ?= mcs-api-controller:latest 22 | # Need v1 to support defaults in CRDs, unfortunately limiting us to k8s 1.16+ 23 | CRD_OPTIONS ?= "crd:crdVersions=v1" 24 | 25 | CONTROLLER_GEN=go -C tools run sigs.k8s.io/controller-tools/cmd/controller-gen 26 | # enable Go modules 27 | export GO111MODULE=on 28 | 29 | .PHONY: all 30 | all: generate manifests controller verify 31 | 32 | .PHONY: e2e-test 33 | e2e-test: export MCS_CONTROLLER_IMAGE := $(IMG) 34 | e2e-test: docker-build 35 | ./scripts/e2e-test.sh 36 | 37 | .PHONY: demo 38 | demo: export MCS_CONTROLLER_IMAGE := $(IMG) 39 | demo: docker-build 40 | ./scripts/up.sh 41 | ./demo/demo.sh 42 | ./scripts/down.sh 43 | 44 | # Build controller binary 45 | .PHONY: controller 46 | controller: generate fmt vet 47 | go -C controllers build -o $(ROOT)/bin/manager cmd/servicecontroller/servicecontroller.go 48 | 49 | # Run go fmt against code 50 | .PHONY: fmt 51 | fmt: 52 | for m in . conformance controllers e2e; do go -C $$m fmt ./...; done 53 | 54 | # Run go vet against code 55 | .PHONY: vet 56 | vet: 57 | for m in . conformance controllers e2e; do go -C $$m vet ./...; done 58 | 59 | # Run generators for Deepcopy funcs and CRDs 60 | .PHONY: generate 61 | generate: 62 | ./hack/update-codegen.sh 63 | $(CONTROLLER_GEN) object:headerFile=$(ROOT)/hack/boilerplate.go.txt paths="$(ROOT)/..." 64 | 65 | # Generate manifests e.g. CRD, RBAC etc. 66 | .PHONY: manifests 67 | manifests: 68 | $(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" 69 | 70 | # Run tests 71 | .PHONY: test 72 | test: generate fmt vet manifests 73 | for m in . controllers; do go -C $$m test ./... -coverprofile cover.out; done 74 | 75 | # Install CRD's and example resources to a pre-existing cluster. 76 | .PHONY: install 77 | install: manifests crd 78 | 79 | # Remove installed CRD's and CR's. 80 | .PHONY: uninstall 81 | uninstall: 82 | ./hack/delete-crds.sh 83 | 84 | # Run static analysis. 85 | .PHONY: verify 86 | verify: 87 | ./hack/verify-all.sh -v 88 | 89 | # Build docker containers 90 | .PHONY: docker-build 91 | docker-build: generate fmt vet manifests 92 | docker build . -t ${IMG} 93 | 94 | # Push the docker image 95 | .PHONY: docker-push 96 | docker-push: docker-build 97 | docker push ${IMG} 98 | 99 | # Run against the configured Kubernetes cluster in ~/.kube/config 100 | run: generate fmt vet manifests 101 | go run ./cmd/servicecontroller/servicecontroller.go 102 | -------------------------------------------------------------------------------- /controllers/controllers_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math/rand" 23 | "path/filepath" 24 | "testing" 25 | "time" 26 | 27 | . "github.com/onsi/ginkgo/v2" 28 | . "github.com/onsi/gomega" 29 | v1 "k8s.io/api/core/v1" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/apimachinery/pkg/runtime" 32 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 33 | "k8s.io/client-go/rest" 34 | "k8s.io/client-go/tools/clientcmd" 35 | ctrl "sigs.k8s.io/controller-runtime" 36 | "sigs.k8s.io/controller-runtime/pkg/client" 37 | "sigs.k8s.io/controller-runtime/pkg/envtest" 38 | "sigs.k8s.io/controller-runtime/pkg/log" 39 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 40 | "sigs.k8s.io/kind/pkg/cluster" 41 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 42 | ) 43 | 44 | const ( 45 | clusterName = "test-cluster" 46 | ) 47 | 48 | var ( 49 | cfg *rest.Config 50 | k8s client.Client 51 | env *envtest.Environment 52 | clusterProvider *cluster.Provider 53 | testNS string 54 | ) 55 | 56 | var _ = BeforeSuite(func(done Done) { 57 | rand.Seed(GinkgoRandomSeed()) 58 | log.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter))) 59 | // Use Kind for a more up-to-date K8s 60 | clusterProvider = cluster.NewProvider() 61 | Expect(clusterProvider.Create(clusterName)).To(Succeed()) 62 | kubeconfig, err := clusterProvider.KubeConfig(clusterName, false) 63 | Expect(err).ToNot(HaveOccurred()) 64 | 65 | cfg, err := clientcmd.RESTConfigFromKubeConfig([]byte(kubeconfig)) 66 | Expect(err).ToNot(HaveOccurred()) 67 | scheme := runtime.NewScheme() 68 | Expect(clientgoscheme.AddToScheme(scheme)).To(Succeed()) 69 | Expect(v1alpha1.AddToScheme(scheme)).To(Succeed()) 70 | Expect(err).ToNot(HaveOccurred()) 71 | existingCluster := true 72 | env = &envtest.Environment{ 73 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd")}, 74 | UseExistingCluster: &existingCluster, 75 | Config: cfg, 76 | } 77 | cfg, err = env.Start() 78 | Expect(err).ToNot(HaveOccurred()) 79 | Expect(cfg).ToNot(BeNil()) 80 | 81 | k8s, err = client.New(cfg, client.Options{Scheme: scheme}) 82 | Expect(err).ToNot(HaveOccurred()) 83 | Expect(k8s).ToNot(BeNil()) 84 | 85 | testNS = fmt.Sprintf("test-%v", time.Now().Unix()) 86 | Expect(k8s.Create(context.Background(), &v1.Namespace{ 87 | ObjectMeta: metav1.ObjectMeta{ 88 | Name: testNS, 89 | }, 90 | })).To(Succeed()) 91 | 92 | opts := ctrl.Options{ 93 | Scheme: scheme, 94 | } 95 | 96 | go Start(context.TODO(), cfg, log.Log, opts) 97 | close(done) 98 | }) 99 | 100 | var _ = AfterSuite(func() { 101 | Expect(clusterProvider.Delete(clusterName, "")).To(Succeed()) 102 | err := env.Stop() 103 | Expect(err).ToNot(HaveOccurred()) 104 | }) 105 | 106 | func TestControllers(t *testing.T) { 107 | RegisterFailHandler(Fail) 108 | } 109 | -------------------------------------------------------------------------------- /controllers/go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/mcs-api/controllers 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.2 7 | github.com/onsi/ginkgo/v2 v2.22.0 8 | github.com/onsi/gomega v1.36.1 9 | k8s.io/api v0.32.5 10 | k8s.io/apimachinery v0.32.5 11 | k8s.io/client-go v0.32.5 12 | sigs.k8s.io/controller-runtime v0.20.4 13 | sigs.k8s.io/kind v0.23.0 14 | sigs.k8s.io/mcs-api v0.3.0 15 | ) 16 | 17 | replace sigs.k8s.io/mcs-api => .. 18 | 19 | require ( 20 | github.com/BurntSushi/toml v1.0.0 // indirect 21 | github.com/alessio/shellescape v1.4.1 // indirect 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 25 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 26 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 27 | github.com/fsnotify/fsnotify v1.7.0 // indirect 28 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 29 | github.com/go-logr/zapr v1.3.0 // indirect 30 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 31 | github.com/go-openapi/jsonreference v0.20.2 // indirect 32 | github.com/go-openapi/swag v0.23.0 // indirect 33 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/golang/protobuf v1.5.4 // indirect 36 | github.com/google/btree v1.1.3 // indirect 37 | github.com/google/gnostic-models v0.6.8 // indirect 38 | github.com/google/go-cmp v0.6.0 // indirect 39 | github.com/google/gofuzz v1.2.0 // indirect 40 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect 41 | github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect 42 | github.com/google/uuid v1.6.0 // indirect 43 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 44 | github.com/josharian/intern v1.0.0 // indirect 45 | github.com/json-iterator/go v1.1.12 // indirect 46 | github.com/mailru/easyjson v0.7.7 // indirect 47 | github.com/mattn/go-isatty v0.0.20 // indirect 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 49 | github.com/modern-go/reflect2 v1.0.2 // indirect 50 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 51 | github.com/pelletier/go-toml v1.9.4 // indirect 52 | github.com/pkg/errors v0.9.1 // indirect 53 | github.com/prometheus/client_golang v1.19.1 // indirect 54 | github.com/prometheus/client_model v0.6.1 // indirect 55 | github.com/prometheus/common v0.55.0 // indirect 56 | github.com/prometheus/procfs v0.15.1 // indirect 57 | github.com/spf13/cobra v1.8.1 // indirect 58 | github.com/spf13/pflag v1.0.5 // indirect 59 | github.com/x448/float16 v0.8.4 // indirect 60 | go.uber.org/multierr v1.11.0 // indirect 61 | go.uber.org/zap v1.27.0 // indirect 62 | golang.org/x/net v0.30.0 // indirect 63 | golang.org/x/oauth2 v0.23.0 // indirect 64 | golang.org/x/sync v0.8.0 // indirect 65 | golang.org/x/sys v0.26.0 // indirect 66 | golang.org/x/term v0.25.0 // indirect 67 | golang.org/x/text v0.19.0 // indirect 68 | golang.org/x/time v0.7.0 // indirect 69 | golang.org/x/tools v0.26.0 // indirect 70 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 71 | google.golang.org/protobuf v1.35.1 // indirect 72 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 73 | gopkg.in/inf.v0 v0.9.1 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | k8s.io/apiextensions-apiserver v0.32.1 // indirect 76 | k8s.io/klog/v2 v2.130.1 // indirect 77 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 78 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 79 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 80 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 81 | sigs.k8s.io/yaml v1.4.0 // indirect 82 | ) 83 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | http "net/http" 23 | 24 | rest "k8s.io/client-go/rest" 25 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 26 | scheme "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/scheme" 27 | ) 28 | 29 | type MulticlusterV1alpha1Interface interface { 30 | RESTClient() rest.Interface 31 | ServiceExportsGetter 32 | ServiceImportsGetter 33 | } 34 | 35 | // MulticlusterV1alpha1Client is used to interact with features provided by the multicluster.x-k8s.io group. 36 | type MulticlusterV1alpha1Client struct { 37 | restClient rest.Interface 38 | } 39 | 40 | func (c *MulticlusterV1alpha1Client) ServiceExports(namespace string) ServiceExportInterface { 41 | return newServiceExports(c, namespace) 42 | } 43 | 44 | func (c *MulticlusterV1alpha1Client) ServiceImports(namespace string) ServiceImportInterface { 45 | return newServiceImports(c, namespace) 46 | } 47 | 48 | // NewForConfig creates a new MulticlusterV1alpha1Client for the given config. 49 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 50 | // where httpClient was generated with rest.HTTPClientFor(c). 51 | func NewForConfig(c *rest.Config) (*MulticlusterV1alpha1Client, error) { 52 | config := *c 53 | if err := setConfigDefaults(&config); err != nil { 54 | return nil, err 55 | } 56 | httpClient, err := rest.HTTPClientFor(&config) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return NewForConfigAndClient(&config, httpClient) 61 | } 62 | 63 | // NewForConfigAndClient creates a new MulticlusterV1alpha1Client for the given config and http client. 64 | // Note the http client provided takes precedence over the configured transport values. 65 | func NewForConfigAndClient(c *rest.Config, h *http.Client) (*MulticlusterV1alpha1Client, error) { 66 | config := *c 67 | if err := setConfigDefaults(&config); err != nil { 68 | return nil, err 69 | } 70 | client, err := rest.RESTClientForConfigAndClient(&config, h) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return &MulticlusterV1alpha1Client{client}, nil 75 | } 76 | 77 | // NewForConfigOrDie creates a new MulticlusterV1alpha1Client for the given config and 78 | // panics if there is an error in the config. 79 | func NewForConfigOrDie(c *rest.Config) *MulticlusterV1alpha1Client { 80 | client, err := NewForConfig(c) 81 | if err != nil { 82 | panic(err) 83 | } 84 | return client 85 | } 86 | 87 | // New creates a new MulticlusterV1alpha1Client for the given RESTClient. 88 | func New(c rest.Interface) *MulticlusterV1alpha1Client { 89 | return &MulticlusterV1alpha1Client{c} 90 | } 91 | 92 | func setConfigDefaults(config *rest.Config) error { 93 | gv := apisv1alpha1.SchemeGroupVersion 94 | config.GroupVersion = &gv 95 | config.APIPath = "/apis" 96 | config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() 97 | 98 | if config.UserAgent == "" { 99 | config.UserAgent = rest.DefaultKubernetesUserAgent() 100 | } 101 | 102 | return nil 103 | } 104 | 105 | // RESTClient returns a RESTClient that is used to communicate 106 | // with API server by this client implementation. 107 | func (c *MulticlusterV1alpha1Client) RESTClient() rest.Interface { 108 | if c == nil { 109 | return nil 110 | } 111 | return c.restClient 112 | } 113 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/apis/v1alpha1/serviceexport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | context "context" 23 | time "time" 24 | 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | watch "k8s.io/apimachinery/pkg/watch" 28 | cache "k8s.io/client-go/tools/cache" 29 | pkgapisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 30 | versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" 31 | internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" 32 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/listers/apis/v1alpha1" 33 | ) 34 | 35 | // ServiceExportInformer provides access to a shared informer and lister for 36 | // ServiceExports. 37 | type ServiceExportInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() apisv1alpha1.ServiceExportLister 40 | } 41 | 42 | type serviceExportInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewServiceExportInformer constructs a new informer for ServiceExport type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewServiceExportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredServiceExportInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredServiceExportInformer constructs a new informer for ServiceExport type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredServiceExportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.MulticlusterV1alpha1().ServiceExports(namespace).List(context.TODO(), options) 66 | }, 67 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.MulticlusterV1alpha1().ServiceExports(namespace).Watch(context.TODO(), options) 72 | }, 73 | }, 74 | &pkgapisv1alpha1.ServiceExport{}, 75 | resyncPeriod, 76 | indexers, 77 | ) 78 | } 79 | 80 | func (f *serviceExportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 81 | return NewFilteredServiceExportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 82 | } 83 | 84 | func (f *serviceExportInformer) Informer() cache.SharedIndexInformer { 85 | return f.factory.InformerFor(&pkgapisv1alpha1.ServiceExport{}, f.defaultInformer) 86 | } 87 | 88 | func (f *serviceExportInformer) Lister() apisv1alpha1.ServiceExportLister { 89 | return apisv1alpha1.NewServiceExportLister(f.Informer().GetIndexer()) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/apis/v1alpha1/serviceimport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | context "context" 23 | time "time" 24 | 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | watch "k8s.io/apimachinery/pkg/watch" 28 | cache "k8s.io/client-go/tools/cache" 29 | pkgapisv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 30 | versioned "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" 31 | internalinterfaces "sigs.k8s.io/mcs-api/pkg/client/informers/externalversions/internalinterfaces" 32 | apisv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/listers/apis/v1alpha1" 33 | ) 34 | 35 | // ServiceImportInformer provides access to a shared informer and lister for 36 | // ServiceImports. 37 | type ServiceImportInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() apisv1alpha1.ServiceImportLister 40 | } 41 | 42 | type serviceImportInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewServiceImportInformer constructs a new informer for ServiceImport type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewServiceImportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredServiceImportInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredServiceImportInformer constructs a new informer for ServiceImport type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredServiceImportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.MulticlusterV1alpha1().ServiceImports(namespace).List(context.TODO(), options) 66 | }, 67 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.MulticlusterV1alpha1().ServiceImports(namespace).Watch(context.TODO(), options) 72 | }, 73 | }, 74 | &pkgapisv1alpha1.ServiceImport{}, 75 | resyncPeriod, 76 | indexers, 77 | ) 78 | } 79 | 80 | func (f *serviceImportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 81 | return NewFilteredServiceImportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 82 | } 83 | 84 | func (f *serviceImportInformer) Informer() cache.SharedIndexInformer { 85 | return f.factory.InformerFor(&pkgapisv1alpha1.ServiceImport{}, f.defaultInformer) 86 | } 87 | 88 | func (f *serviceImportInformer) Lister() apisv1alpha1.ServiceImportLister { 89 | return apisv1alpha1.NewServiceImportLister(f.Informer().GetIndexer()) 90 | } 91 | -------------------------------------------------------------------------------- /scripts/up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | cd $(dirname ${BASH_SOURCE}) 18 | 19 | . ./util.sh 20 | 21 | set -e 22 | 23 | kind() { 24 | go -C ../controllers run sigs.k8s.io/kind "$@" 25 | } 26 | 27 | c1=${CLUSTER1:-c1} 28 | c2=${CLUSTER2:-c2} 29 | kubeconfig1=${KUBECONFIG1:-"$PWD/${c1}.kubeconfig"} 30 | kubeconfig2=${KUBECONFIG2:-"$PWD/${c2}.kubeconfig"} 31 | controller_image=${MCS_CONTROLLER_IMAGE:-"mcs-api-controller"} 32 | 33 | k1="kubectl --kubeconfig ${kubeconfig1}" 34 | k2="kubectl --kubeconfig ${kubeconfig2}" 35 | 36 | if [ ! -z "${BUILD_CONTROLLER}" ] || [ -z "$(docker images mcs-api-controller -q)" ]; then 37 | pushd ../ 38 | make docker-build 39 | popd 40 | fi 41 | 42 | kind create cluster --name "${c1}" --config "$PWD/${c1}.yaml" 43 | kind create cluster --name "${c2}" --config "$PWD/${c2}.yaml" 44 | 45 | kind get kubeconfig --name "${c1}" > "${kubeconfig1}" 46 | kind get kubeconfig --name "${c2}" > "${kubeconfig2}" 47 | 48 | kind load docker-image "${controller_image}" --name "${c1}" 49 | kind load docker-image "${controller_image}" --name "${c2}" 50 | 51 | echo "Configuring CoreDNS" 52 | function update_coredns() { 53 | kubectl --kubeconfig ${1} apply -f ../config/crd 54 | kubectl --kubeconfig ${1} patch clusterrole system:coredns --type json --patch-file coredns-rbac.json 55 | # Patching Corefile based on Cilium documentation: https://docs.cilium.io/en/latest/network/clustermesh/mcsapi/#prerequisites 56 | kubectl --kubeconfig ${1} get configmap -n kube-system coredns -o yaml | \ 57 | sed -e 's/cluster\.local/cluster.local clusterset.local/g' | \ 58 | sed -E 's/^(.*)kubernetes(.*)\{/\1kubernetes\2{\n\1 multicluster clusterset.local/' | \ 59 | kubectl --kubeconfig ${1} replace -f- 60 | kubectl --kubeconfig ${1} rollout restart deploy -n kube-system coredns 61 | } 62 | update_coredns ${kubeconfig1} 63 | update_coredns ${kubeconfig2} 64 | 65 | function pod_cidrs() { 66 | kubectl --kubeconfig "${1}" get nodes -o jsonpath='{range .items[*]}{.spec.podCIDR}{"\n"}' 67 | } 68 | 69 | function add_routes() { 70 | unset IFS 71 | routes=$(kubectl --kubeconfig ${3} get node ${2} -o jsonpath='ip route add {.spec.podCIDR} via {.status.addresses[?(.type=="InternalIP")].address}') 72 | echo "Connecting cluster ${1} to ${2}" 73 | 74 | IFS=$'\n' 75 | for n in $(kind get nodes --name "${1}"); do 76 | for r in $routes; do 77 | eval "docker exec $n $r" 78 | done 79 | done 80 | unset IFS 81 | } 82 | 83 | waitfor pod_cidrs ${kubeconfig1} 84 | waitfor pod_cidrs ${kubeconfig2} 85 | 86 | echo "Connecting cluster networks..." 87 | add_routes "${c1}" "${c2}-control-plane" "${kubeconfig2}" 88 | add_routes "${c2}" "${c1}-control-plane" "${kubeconfig1}" 89 | echo "Cluster networks connected" 90 | 91 | ${k1} apply -f ../config/rbac 92 | ${k2} apply -f ../config/rbac 93 | 94 | ${k1} create sa mcs-api-controller 95 | ${k1} create clusterrolebinding mcs-api-binding --clusterrole=mcs-derived-service-manager --serviceaccount=default:mcs-api-controller 96 | ${k1} run --image "${controller_image}" --image-pull-policy=Never mcs-api-controller --overrides='{ "spec": { "serviceAccount": "mcs-api-controller" } }' 97 | 98 | ${k2} create sa mcs-api-controller 99 | ${k2} create clusterrolebinding mcs-api-binding --clusterrole=mcs-derived-service-manager --serviceaccount=default:mcs-api-controller 100 | ${k2} run --image "${controller_image}" --image-pull-policy=Never mcs-api-controller --overrides='{ "spec": { "serviceAccount": "mcs-api-controller" } }' 101 | -------------------------------------------------------------------------------- /demo/demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2020 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Resolve the absolute path to this script directory. 18 | cd $(dirname "${BASH_SOURCE[0]}") 19 | demo_dir=$(realpath "$(pwd)") 20 | scripts_dir=$(realpath "$(pwd)/../scripts") 21 | 22 | . "${demo_dir}/udemo.sh" 23 | . "${scripts_dir}/util.sh" 24 | 25 | DEMO_AUTO_RUN=true 26 | 27 | kubeconfig1="${KUBECONFIG1:-${scripts_dir}/c1.kubeconfig}" 28 | kubeconfig2="${KUBECONFIG2:-${scripts_dir}/c2.kubeconfig}" 29 | 30 | k1="kubectl --kubeconfig ${kubeconfig1}" 31 | k2="kubectl --kubeconfig ${kubeconfig2}" 32 | 33 | desc "Setup our demo namespace" 34 | run "${k1} create ns demo" 35 | run "${k2} create ns demo" 36 | 37 | c1_pane=$(tmux split-window -h -d -P) 38 | 39 | function cleanup() { 40 | tmux kill-pane -t "$c1_pane" 41 | } 42 | trap cleanup EXIT 43 | 44 | tmux send -t "$c1_pane" "${k1} logs -f mcs-api-controller" Enter 45 | 46 | desc "Create our service in each cluster" 47 | run "${k1} apply -f ${demo_dir}/yaml/dep1.yaml -f ${demo_dir}/yaml/svc.yaml" 48 | run "${k2} apply -f ${demo_dir}/yaml/dep2.yaml -f ${demo_dir}/yaml/svc.yaml" 49 | run "${k1} get endpointslice -n demo" 50 | 51 | desc "Lets look at some requests to the service in cluster 1" 52 | run "${k1} -n demo run -i --rm --restart=Never --image=jeremyot/request:0a40de8 request -- --duration=5s --address=serve.demo.svc.cluster.local" 53 | 54 | desc "Ok, looks normal. Let's import the service from our other cluster" 55 | ep_1=$(${k1} get endpointslice -n demo -l 'kubernetes.io/service-name=serve' --template="{{(index .items 0).metadata.name}}") 56 | ep_2=$(${k2} get endpointslice -n demo -l 'kubernetes.io/service-name=serve' --template="{{(index .items 0).metadata.name}}") 57 | 58 | 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" 59 | 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" 60 | run "${k1} apply -f ${demo_dir}/yaml/serviceimport.yaml -f ${demo_dir}/yaml/slice-1.tmp -f ${demo_dir}/yaml/slice-2.tmp" 61 | run "${k2} apply -f ${demo_dir}/yaml/serviceimport.yaml -f ${demo_dir}/yaml/slice-1.tmp -f ${demo_dir}/yaml/slice-2.tmp" 62 | 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" 63 | 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" 64 | 65 | desc "See what we've created..." 66 | run "${k1} get -n demo serviceimports" 67 | run "${k1} get -n demo endpointslice" 68 | run "${k1} get -n demo service" 69 | 70 | function import_ip() { 71 | ${k1} get serviceimport -n demo -o go-template --template='{{index (index .items 0).spec.ips 0}}' 72 | } 73 | 74 | waitfor import_ip 75 | 76 | vip=$(${k1} get serviceimport -n demo -o go-template --template='{{index (index .items 0).spec.ips 0}}') 77 | desc "Now grab the multi-cluster VIP from the serviceimport..." 78 | run "${k1} get serviceimport -n demo -o go-template --template='{{index (index .items 0).spec.ips 0}}{{\"\n\"}}'" 79 | desc "...and connect to it" 80 | run "${k1} -n demo run -i --rm --restart=Never --image=jeremyot/request:0a40de8 request -- --duration=10s --address=${vip}" 81 | run "${k1} -n demo run -i --rm --restart=Never --image=jeremyot/request:0a40de8 request -- --duration=10s --address=serve.demo.svc.clusterset.local" 82 | desc "We have a multi-cluster service!" 83 | desc "See for yourself" 84 | desc "Cluster 1: kubectl --kubeconfig ${kubeconfig1} -n demo" 85 | desc "Cluster 2: kubectl --kubeconfig ${kubeconfig2} -n demo" 86 | desc "(Enter to exit)" 87 | read -s 88 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package versioned 20 | 21 | import ( 22 | fmt "fmt" 23 | http "net/http" 24 | 25 | discovery "k8s.io/client-go/discovery" 26 | rest "k8s.io/client-go/rest" 27 | flowcontrol "k8s.io/client-go/util/flowcontrol" 28 | multiclusterv1alpha1 "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned/typed/apis/v1alpha1" 29 | ) 30 | 31 | type Interface interface { 32 | Discovery() discovery.DiscoveryInterface 33 | MulticlusterV1alpha1() multiclusterv1alpha1.MulticlusterV1alpha1Interface 34 | } 35 | 36 | // Clientset contains the clients for groups. 37 | type Clientset struct { 38 | *discovery.DiscoveryClient 39 | multiclusterV1alpha1 *multiclusterv1alpha1.MulticlusterV1alpha1Client 40 | } 41 | 42 | // MulticlusterV1alpha1 retrieves the MulticlusterV1alpha1Client 43 | func (c *Clientset) MulticlusterV1alpha1() multiclusterv1alpha1.MulticlusterV1alpha1Interface { 44 | return c.multiclusterV1alpha1 45 | } 46 | 47 | // Discovery retrieves the DiscoveryClient 48 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 49 | if c == nil { 50 | return nil 51 | } 52 | return c.DiscoveryClient 53 | } 54 | 55 | // NewForConfig creates a new Clientset for the given config. 56 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 57 | // NewForConfig will generate a rate-limiter in configShallowCopy. 58 | // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), 59 | // where httpClient was generated with rest.HTTPClientFor(c). 60 | func NewForConfig(c *rest.Config) (*Clientset, error) { 61 | configShallowCopy := *c 62 | 63 | if configShallowCopy.UserAgent == "" { 64 | configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() 65 | } 66 | 67 | // share the transport between all clients 68 | httpClient, err := rest.HTTPClientFor(&configShallowCopy) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return NewForConfigAndClient(&configShallowCopy, httpClient) 74 | } 75 | 76 | // NewForConfigAndClient creates a new Clientset for the given config and http client. 77 | // Note the http client provided takes precedence over the configured transport values. 78 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 79 | // NewForConfigAndClient will generate a rate-limiter in configShallowCopy. 80 | func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { 81 | configShallowCopy := *c 82 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 83 | if configShallowCopy.Burst <= 0 { 84 | 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") 85 | } 86 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 87 | } 88 | 89 | var cs Clientset 90 | var err error 91 | cs.multiclusterV1alpha1, err = multiclusterv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) 97 | if err != nil { 98 | return nil, err 99 | } 100 | return &cs, nil 101 | } 102 | 103 | // NewForConfigOrDie creates a new Clientset for the given config and 104 | // panics if there is an error in the config. 105 | func NewForConfigOrDie(c *rest.Config) *Clientset { 106 | cs, err := NewForConfig(c) 107 | if err != nil { 108 | panic(err) 109 | } 110 | return cs 111 | } 112 | 113 | // New creates a new Clientset for the given RESTClient. 114 | func New(c rest.Interface) *Clientset { 115 | var cs Clientset 116 | cs.multiclusterV1alpha1 = multiclusterv1alpha1.New(c) 117 | 118 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 119 | return &cs 120 | } 121 | -------------------------------------------------------------------------------- /controllers/endpointslice_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math/rand" 23 | "time" 24 | 25 | . "github.com/onsi/ginkgo/v2" 26 | . "github.com/onsi/gomega" 27 | discoveryv1 "k8s.io/api/discovery/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/types" 30 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 31 | ) 32 | 33 | var _ = Describe("EndpointSlice", func() { 34 | ctx := context.Background() 35 | Context("should be ignored", func() { 36 | Specify("when not multi-cluster", func() { 37 | Expect(shouldIgnoreEndpointSlice(&discoveryv1.EndpointSlice{ 38 | ObjectMeta: metav1.ObjectMeta{ 39 | Namespace: testNS, 40 | Name: "no-mc-service", 41 | }, 42 | AddressType: discoveryv1.AddressTypeIPv4, 43 | })).To(BeTrue()) 44 | }) 45 | Specify("when deleted", func() { 46 | Expect(shouldIgnoreEndpointSlice(&discoveryv1.EndpointSlice{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Namespace: testNS, 49 | Name: "deleted", 50 | DeletionTimestamp: &metav1.Time{Time: time.Now()}, 51 | }, 52 | AddressType: discoveryv1.AddressTypeIPv4, 53 | })).To(BeTrue()) 54 | }) 55 | }) 56 | Context("created with mc label", func() { 57 | var ( 58 | serviceName types.NamespacedName 59 | derivedServiceName types.NamespacedName 60 | sliceName types.NamespacedName 61 | epSlice discoveryv1.EndpointSlice 62 | ) 63 | BeforeEach(func() { 64 | serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} 65 | derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} 66 | sliceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("slice-%v", rand.Uint64())} 67 | epSlice = discoveryv1.EndpointSlice{ 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Namespace: testNS, 70 | Name: sliceName.Name, 71 | Labels: map[string]string{ 72 | v1alpha1.LabelServiceName: serviceName.Name, 73 | }, 74 | }, 75 | AddressType: discoveryv1.AddressTypeIPv4, 76 | } 77 | Expect(k8s.Create(ctx, &epSlice)).To(Succeed()) 78 | }) 79 | It("has correct label", func() { 80 | Eventually(func() string { 81 | var eps discoveryv1.EndpointSlice 82 | Expect(k8s.Get(ctx, sliceName, &eps)).Should(Succeed()) 83 | return eps.Labels[discoveryv1.LabelServiceName] 84 | }).Should(Equal(derivedServiceName.Name)) 85 | }) 86 | }) 87 | Context("created with wrong label", func() { 88 | var ( 89 | serviceName types.NamespacedName 90 | derivedServiceName types.NamespacedName 91 | sliceName types.NamespacedName 92 | epSlice discoveryv1.EndpointSlice 93 | ) 94 | BeforeEach(func() { 95 | serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} 96 | derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} 97 | sliceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("slice-%v", rand.Uint64())} 98 | epSlice = discoveryv1.EndpointSlice{ 99 | ObjectMeta: metav1.ObjectMeta{ 100 | Namespace: testNS, 101 | Name: sliceName.Name, 102 | Labels: map[string]string{ 103 | v1alpha1.LabelServiceName: serviceName.Name, 104 | discoveryv1.LabelServiceName: serviceName.Name, 105 | }, 106 | }, 107 | AddressType: discoveryv1.AddressTypeIPv4, 108 | } 109 | Expect(k8s.Create(ctx, &epSlice)).To(Succeed()) 110 | }) 111 | It("has correct label", func() { 112 | Eventually(func() string { 113 | var eps discoveryv1.EndpointSlice 114 | Expect(k8s.Get(ctx, sliceName, &eps)).Should(Succeed()) 115 | return eps.Labels[discoveryv1.LabelServiceName] 116 | }).Should(Equal(derivedServiceName.Name)) 117 | }) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /conformance/connectivity.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package conformance 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "strings" 23 | 24 | . "github.com/onsi/ginkgo/v2" 25 | . "github.com/onsi/gomega" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | ) 28 | 29 | var _ = Describe("", func() { 30 | t := newTestDriver() 31 | 32 | BeforeEach(func() { 33 | t.autoExportService = false 34 | }) 35 | 36 | Context("Connectivity to a service that is not exported", func() { 37 | It("should be inaccessible", Label(RequiredLabel), func() { 38 | AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#exporting-services") 39 | By("attempting to access the remote service", func() { 40 | By("issuing a request from all clusters", func() { 41 | command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.clusterset.local 42", 42 | t.helloService.Name, t.namespace)} 43 | 44 | // Run on all clusters 45 | for _, client := range clients { 46 | // Repeat multiple times 47 | for i := 0; i < 20; i++ { 48 | Expect(t.execCmdOnRequestPod(&client, command)).NotTo(ContainSubstring("pod ip"), reportNonConformant("")) 49 | } 50 | } 51 | }) 52 | }) 53 | }) 54 | }) 55 | 56 | Context("Connectivity to an exported ClusterIP service", func() { 57 | It("should be accessible through DNS", Label(OptionalLabel, ConnectivityLabel, ClusterIPLabel), func() { 58 | AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") 59 | By("Exporting the service", func() { 60 | // On the "remote" cluster 61 | t.createServiceExport(&clients[0], newHelloServiceExport()) 62 | }) 63 | By("Issuing a request from all clusters", func() { 64 | // Run on all clusters 65 | command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.clusterset.local 42", 66 | t.helloService.Name, t.namespace)} 67 | 68 | for _, client := range clients { 69 | By(fmt.Sprintf("Executing command %q on cluster %q", strings.Join(command, " "), client.name)) 70 | 71 | t.awaitCmdOutputMatches(&client, command, "pod ip", 1, reportNonConformant("")) 72 | } 73 | }) 74 | }) 75 | }) 76 | 77 | Context("Connectivity to a ClusterIP service existing in two clusters but exported from one", func() { 78 | BeforeEach(func() { 79 | requireTwoClusters() 80 | }) 81 | 82 | JustBeforeEach(func() { 83 | t.deployHelloService(&clients[1], newHelloService()) 84 | }) 85 | 86 | It("should only access the exporting cluster", Label(OptionalLabel, ConnectivityLabel, ClusterIPLabel), func() { 87 | AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/1645-multi-cluster-services-api/README.md#exporting-services") 88 | 89 | By(fmt.Sprintf("Exporting the service on cluster %q", clients[0].name)) 90 | 91 | t.createServiceExport(&clients[0], newHelloServiceExport()) 92 | 93 | By(fmt.Sprintf("Awaiting service deployment pod IP on cluster %q", clients[0].name)) 94 | 95 | servicePodIP := "" 96 | 97 | Eventually(func() string { 98 | pods, err := clients[0].k8s.CoreV1().Pods(t.namespace).List(context.TODO(), metav1.ListOptions{ 99 | LabelSelector: metav1.FormatLabelSelector(newHelloDeployment().Spec.Selector), 100 | }) 101 | Expect(err).ToNot(HaveOccurred()) 102 | 103 | if len(pods.Items) > 0 { 104 | servicePodIP = pods.Items[0].Status.PodIP 105 | } 106 | 107 | return servicePodIP 108 | }, 20, 1).ShouldNot(BeEmpty(), "Service deployment pod was not allocated an IP") 109 | 110 | By(fmt.Sprintf("Retrieved service deployment pod IP %q", servicePodIP)) 111 | 112 | command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.clusterset.local 42", 113 | t.helloService.Name, t.namespace)} 114 | 115 | for _, client := range clients { 116 | By(fmt.Sprintf("Executing command %q on cluster %q", strings.Join(command, " "), client.name)) 117 | 118 | t.awaitCmdOutputMatches(&client, command, servicePodIP, 10, reportNonConformant("")) 119 | } 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /conformance/endpoint_slice.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package conformance 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | discoveryv1 "k8s.io/api/discovery/v1" 26 | apierrors "k8s.io/apimachinery/pkg/api/errors" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 29 | ) 30 | 31 | // K8sEndpointSliceManagedByName is the name used for endpoint slices managed by the Kubernetes controller 32 | const K8sEndpointSliceManagedByName = "endpointslice-controller.k8s.io" 33 | 34 | var _ = Describe("", Label(OptionalLabel, EndpointSliceLabel), func() { 35 | t := newTestDriver() 36 | 37 | Specify("Exporting a service should create an MCS EndpointSlice in the service's namespace in each cluster with the "+ 38 | "required MCS labels. Unexporting should delete the EndpointSlice.", func() { 39 | AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#using-endpointslice-objects-to-track-endpoints") 40 | 41 | endpointSlices := make([]*discoveryv1.EndpointSlice, len(clients)) 42 | 43 | for i, client := range clients { 44 | eps := t.awaitMCSEndpointSlice(&client, discoveryv1.AddressTypeIPv4, nil, reportNonConformant(fmt.Sprintf( 45 | "an MCS EndpointSlice was not found on cluster %q. An MCS EndpointSlice is identified by the presence "+ 46 | "of the required MCS labels (%q and %q). "+ 47 | "If the MCS implementation does not use MCS EndpointSlices, you can specify a Ginkgo label filter using "+ 48 | "the %q label where appropriate to skip this test.", 49 | client.name, v1alpha1.LabelServiceName, v1alpha1.LabelSourceCluster, EndpointSliceLabel))) 50 | 51 | endpointSlices[i] = eps 52 | 53 | Expect(eps.Labels).To(HaveKeyWithValue(v1alpha1.LabelServiceName, t.helloService.Name), 54 | reportNonConformant(fmt.Sprintf("the MCS EndpointSlice %q does not contain the %q label referencing the service name", 55 | eps.Name, v1alpha1.LabelServiceName))) 56 | 57 | Expect(eps.Labels).To(HaveKey(discoveryv1.LabelManagedBy), 58 | reportNonConformant(fmt.Sprintf("the MCS EndpointSlice %q does not contain the %q label", 59 | eps.Name, discoveryv1.LabelManagedBy))) 60 | 61 | if !skipVerifyEndpointSliceManagedBy { 62 | Expect(eps.Labels[discoveryv1.LabelManagedBy]).ToNot(Equal(K8sEndpointSliceManagedByName), 63 | reportNonConformant(fmt.Sprintf("the MCS EndpointSlice's %q label must not reference %q", 64 | discoveryv1.LabelManagedBy, K8sEndpointSliceManagedByName))) 65 | } 66 | } 67 | 68 | t.deleteServiceExport(&clients[0]) 69 | 70 | for i, client := range clients { 71 | Eventually(func() bool { 72 | _, err := client.k8s.DiscoveryV1().EndpointSlices(t.namespace).Get(ctx, endpointSlices[i].Name, metav1.GetOptions{}) 73 | return apierrors.IsNotFound(err) 74 | }, 20*time.Second, 100*time.Millisecond).Should(BeTrue(), 75 | reportNonConformant(fmt.Sprintf("the EndpointSlice was not deleted on unexport from cluster %d", i+1))) 76 | } 77 | }) 78 | }) 79 | 80 | func (t *testDriver) awaitMCSEndpointSlice(c *clusterClients, addressType discoveryv1.AddressType, 81 | verify func(Gomega, *discoveryv1.EndpointSlice), desc ...any) *discoveryv1.EndpointSlice { 82 | var endpointSlice *discoveryv1.EndpointSlice 83 | 84 | hasLabel := func(eps *discoveryv1.EndpointSlice, label string) bool { 85 | _, exists := eps.Labels[label] 86 | return exists 87 | } 88 | 89 | Eventually(func(g Gomega) { 90 | list, err := c.k8s.DiscoveryV1().EndpointSlices(t.namespace).List(ctx, metav1.ListOptions{}) 91 | g.Expect(err).ToNot(HaveOccurred(), "Error retrieving EndpointSlices") 92 | 93 | endpointSlice = nil 94 | 95 | for i := range list.Items { 96 | eps := &list.Items[i] 97 | 98 | if hasLabel(eps, v1alpha1.LabelServiceName) && hasLabel(eps, v1alpha1.LabelSourceCluster) && eps.AddressType == addressType { 99 | endpointSlice = eps 100 | 101 | if verify != nil { 102 | verify(g, endpointSlice) 103 | } 104 | } 105 | } 106 | 107 | g.Expect(endpointSlice).ToNot(BeNil(), desc...) 108 | 109 | // The final run succeeded so cancel any prior non-conformance reported. 110 | cancelNonConformanceReport() 111 | }).Within(20 * time.Second).ProbeEvery(100 * time.Millisecond).Should(Succeed()) 112 | 113 | return endpointSlice 114 | } 115 | -------------------------------------------------------------------------------- /conformance/k8s_objects.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package conformance 18 | 19 | import ( 20 | appsv1 "k8s.io/api/apps/v1" 21 | corev1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/utils/ptr" 24 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 25 | ) 26 | 27 | const helloServiceName = "hello" 28 | 29 | func newHelloService() *corev1.Service { 30 | return &corev1.Service{ 31 | ObjectMeta: metav1.ObjectMeta{ 32 | Name: helloServiceName, 33 | Annotations: map[string]string{"dummy-svc": "dummy"}, 34 | Labels: map[string]string{"dummy-svc": "dummy"}, 35 | }, 36 | Spec: corev1.ServiceSpec{ 37 | Selector: map[string]string{ 38 | "app": helloServiceName, 39 | }, 40 | Ports: []corev1.ServicePort{ 41 | { 42 | Name: "tcp", 43 | Port: 42, 44 | Protocol: corev1.ProtocolTCP, 45 | }, 46 | { 47 | Name: "udp", 48 | Port: 42, 49 | Protocol: corev1.ProtocolUDP, 50 | }, 51 | }, 52 | SessionAffinity: corev1.ServiceAffinityClientIP, 53 | SessionAffinityConfig: &corev1.SessionAffinityConfig{ 54 | ClientIP: &corev1.ClientIPConfig{TimeoutSeconds: ptr.To(int32(10))}, 55 | }, 56 | }, 57 | } 58 | } 59 | 60 | func newHelloServiceExport() *v1alpha1.ServiceExport { 61 | return &v1alpha1.ServiceExport{ 62 | ObjectMeta: metav1.ObjectMeta{ 63 | Name: helloServiceName, 64 | Annotations: map[string]string{"dummy-svcexport": "dummy"}, 65 | Labels: map[string]string{"dummy-svcexport": "dummy"}, 66 | }, 67 | } 68 | } 69 | 70 | func podContainers() []corev1.Container { 71 | return []corev1.Container{ 72 | { 73 | Name: "hello-tcp", 74 | Image: "alpine/socat:1.7.4.4", 75 | Args: []string{"-v", "-v", "TCP-LISTEN:42,crlf,reuseaddr,fork", "SYSTEM:echo pod ip $(MY_POD_IP)"}, 76 | Env: []corev1.EnvVar{ 77 | { 78 | Name: "MY_POD_IP", 79 | ValueFrom: &corev1.EnvVarSource{ 80 | FieldRef: &corev1.ObjectFieldSelector{ 81 | FieldPath: "status.podIP", 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | { 88 | Name: "hello-udp", 89 | Image: "alpine/socat:1.7.4.4", 90 | Args: []string{"-v", "-v", "UDP-LISTEN:42,crlf,reuseaddr,fork", "SYSTEM:echo pod ip $(MY_POD_IP)"}, 91 | Env: []corev1.EnvVar{ 92 | { 93 | Name: "MY_POD_IP", 94 | ValueFrom: &corev1.EnvVarSource{ 95 | FieldRef: &corev1.ObjectFieldSelector{ 96 | FieldPath: "status.podIP", 97 | }, 98 | }, 99 | }, 100 | }, 101 | }, 102 | } 103 | } 104 | 105 | func newHelloDeployment() *appsv1.Deployment { 106 | return &appsv1.Deployment{ 107 | ObjectMeta: metav1.ObjectMeta{ 108 | Name: helloServiceName, 109 | }, 110 | Spec: appsv1.DeploymentSpec{ 111 | Replicas: ptr.To(int32(1)), 112 | Selector: &metav1.LabelSelector{ 113 | MatchLabels: map[string]string{ 114 | "app": helloServiceName, 115 | }, 116 | }, 117 | Template: corev1.PodTemplateSpec{ 118 | ObjectMeta: metav1.ObjectMeta{ 119 | Labels: map[string]string{"app": helloServiceName}, 120 | }, 121 | Spec: corev1.PodSpec{ 122 | Containers: podContainers(), 123 | }, 124 | }, 125 | }, 126 | } 127 | } 128 | 129 | func newStatefulSet(replicas int) *appsv1.StatefulSet { 130 | return &appsv1.StatefulSet{ 131 | ObjectMeta: metav1.ObjectMeta{ 132 | Name: helloServiceName + "-ss", 133 | }, 134 | Spec: appsv1.StatefulSetSpec{ 135 | Selector: &metav1.LabelSelector{ 136 | MatchLabels: map[string]string{ 137 | "app": helloServiceName, 138 | }, 139 | }, 140 | ServiceName: helloServiceName, 141 | Replicas: ptr.To(int32(replicas)), 142 | Template: corev1.PodTemplateSpec{ 143 | ObjectMeta: metav1.ObjectMeta{ 144 | Labels: map[string]string{ 145 | "app": helloServiceName, 146 | }, 147 | }, 148 | Spec: corev1.PodSpec{ 149 | Containers: podContainers(), 150 | RestartPolicy: corev1.RestartPolicyAlways, 151 | }, 152 | }, 153 | }, 154 | } 155 | } 156 | 157 | func newRequestPod() *corev1.Pod { 158 | return &corev1.Pod{ 159 | ObjectMeta: metav1.ObjectMeta{ 160 | Name: "request", 161 | Labels: map[string]string{"app": "request"}, 162 | }, 163 | Spec: corev1.PodSpec{ 164 | Containers: []corev1.Container{ 165 | { 166 | Name: "request", 167 | Image: "busybox", 168 | Args: []string{"/bin/sh", "-ec", "while :; do echo '.'; sleep 5 ; done"}, 169 | }, 170 | }, 171 | }, 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /controllers/serviceimport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/go-logr/logr" 23 | v1 "k8s.io/api/core/v1" 24 | apierrors "k8s.io/apimachinery/pkg/api/errors" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/types" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 30 | ) 31 | 32 | // ServiceImportReconciler reconciles a ServiceImport object 33 | type ServiceImportReconciler struct { 34 | client.Client 35 | Log logr.Logger 36 | } 37 | 38 | // +kubebuilder:rbac:groups=multicluster.x-k8s.io,resources=serviceimports,verbs=get;list;watch;update;patch 39 | 40 | func servicePorts(svcImport *v1alpha1.ServiceImport) []v1.ServicePort { 41 | ports := make([]v1.ServicePort, len(svcImport.Spec.Ports)) 42 | for i, p := range svcImport.Spec.Ports { 43 | ports[i] = v1.ServicePort{ 44 | Name: p.Name, 45 | Protocol: p.Protocol, 46 | Port: p.Port, 47 | AppProtocol: p.AppProtocol, 48 | } 49 | } 50 | return ports 51 | } 52 | 53 | func shouldIgnoreImport(svcImport *v1alpha1.ServiceImport) bool { 54 | if svcImport.DeletionTimestamp != nil { 55 | return true 56 | } 57 | if svcImport.Spec.Type != v1alpha1.ClusterSetIP { 58 | return true 59 | } 60 | return false 61 | } 62 | 63 | // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch 64 | // +kubebuilder:rbac:groups=core,resources=services/status,verbs=get;list;watch;create;update;patch 65 | 66 | // Reconcile the changes. 67 | func (r *ServiceImportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 68 | serviceName := derivedName(req.NamespacedName) 69 | log := r.Log.WithValues("serviceimport", req.NamespacedName, "derived", serviceName) 70 | var svcImport v1alpha1.ServiceImport 71 | if err := r.Client.Get(ctx, req.NamespacedName, &svcImport); err != nil { 72 | return ctrl.Result{}, client.IgnoreNotFound(err) 73 | } 74 | if shouldIgnoreImport(&svcImport) { 75 | return ctrl.Result{}, nil 76 | } 77 | 78 | // Ensure the existence of the derived service 79 | var svc v1.Service 80 | if svcImport.Annotations[DerivedServiceAnnotation] == "" { 81 | if svcImport.Annotations == nil { 82 | svcImport.Annotations = map[string]string{} 83 | } 84 | svcImport.Annotations[DerivedServiceAnnotation] = derivedName(req.NamespacedName) 85 | if err := r.Client.Update(ctx, &svcImport); err != nil { 86 | return ctrl.Result{}, err 87 | } 88 | log.Info("added annotation", DerivedServiceAnnotation, svcImport.Annotations[DerivedServiceAnnotation]) 89 | return ctrl.Result{}, nil 90 | } 91 | if err := r.Client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: svcImport.Annotations[DerivedServiceAnnotation]}, &svc); err == nil { 92 | return ctrl.Result{}, nil 93 | } else if !apierrors.IsNotFound(err) { 94 | return ctrl.Result{}, err 95 | } 96 | 97 | svc = v1.Service{ 98 | ObjectMeta: metav1.ObjectMeta{ 99 | Namespace: req.Namespace, 100 | Name: svcImport.Annotations[DerivedServiceAnnotation], 101 | OwnerReferences: []metav1.OwnerReference{ 102 | { 103 | Name: req.Name, 104 | Kind: serviceImportKind, 105 | APIVersion: v1alpha1.GroupVersion.String(), 106 | UID: svcImport.UID, 107 | }, 108 | }, 109 | }, 110 | Spec: v1.ServiceSpec{ 111 | Type: v1.ServiceTypeClusterIP, 112 | Ports: servicePorts(&svcImport), 113 | }, 114 | } 115 | if err := r.Client.Create(ctx, &svc); err != nil { 116 | return ctrl.Result{}, err 117 | } 118 | log.Info("created service") 119 | 120 | if len(svcImport.Spec.IPs) == 0 { 121 | return ctrl.Result{}, nil 122 | } 123 | 124 | // update loadbalanacer status with provided clustersetIPs 125 | ingress := []v1.LoadBalancerIngress{} 126 | for _, ip := range svcImport.Spec.IPs { 127 | ingress = append(ingress, v1.LoadBalancerIngress{ 128 | IP: ip, 129 | }) 130 | } 131 | 132 | svc.Status = v1.ServiceStatus{ 133 | LoadBalancer: v1.LoadBalancerStatus{ 134 | Ingress: ingress, 135 | }, 136 | } 137 | 138 | if err := r.Client.Status().Update(ctx, &svc); err != nil { 139 | return ctrl.Result{}, err 140 | } 141 | 142 | return ctrl.Result{}, nil 143 | } 144 | 145 | // SetupWithManager wires up the controller. 146 | func (r *ServiceImportReconciler) SetupWithManager(mgr ctrl.Manager) error { 147 | return ctrl.NewControllerManagedBy(mgr).For(&v1alpha1.ServiceImport{}).Complete(r) 148 | } 149 | -------------------------------------------------------------------------------- /e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2etest 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "flag" 23 | "math/rand" 24 | "os" 25 | "strconv" 26 | "testing" 27 | 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/labels" 30 | "k8s.io/client-go/kubernetes/scheme" 31 | restclient "k8s.io/client-go/rest" 32 | "k8s.io/client-go/tools/remotecommand" 33 | 34 | . "github.com/onsi/ginkgo/v2" 35 | . "github.com/onsi/gomega" 36 | v1 "k8s.io/api/core/v1" 37 | discoveryv1 "k8s.io/api/discovery/v1" 38 | "k8s.io/client-go/kubernetes" 39 | "k8s.io/client-go/tools/clientcmd" 40 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 41 | mcsclient "sigs.k8s.io/mcs-api/pkg/client/clientset/versioned" 42 | ) 43 | 44 | var ( 45 | kubeconfig1 = flag.String("kubeconfig1", os.Getenv("KUBECONFIG1"), "The path to a kubeconfig for cluster 1") 46 | kubeconfig2 = flag.String("kubeconfig2", os.Getenv("KUBECONFIG2"), "The path to a kubeconfig for cluster 2") 47 | noTearDown = flag.Bool("no-tear-down", tryParseBool(os.Getenv("NO_TEAR_DOWN")), "Don't tear down after test (useful for debugging failures).") 48 | cluster1 clusterClients 49 | cluster2 clusterClients 50 | restcfg1, _ = clientcmd.BuildConfigFromFlags("", *kubeconfig1) 51 | //restcfg2, _ = clientcmd.BuildConfigFromFlags("", *kubeconfig2) 52 | ) 53 | 54 | func tryParseBool(s string) bool { 55 | b, _ := strconv.ParseBool(s) 56 | return b 57 | } 58 | 59 | type clusterClients struct { 60 | k8s kubernetes.Interface 61 | mcs mcsclient.Interface 62 | } 63 | 64 | func TestE2E(t *testing.T) { 65 | flag.Parse() 66 | RegisterFailHandler(Fail) 67 | RunSpecs(t, "E2E Suite") 68 | } 69 | 70 | var _ = BeforeSuite(func() { 71 | rand.Seed(GinkgoRandomSeed()) 72 | 73 | Expect(*kubeconfig1).ToNot(BeEmpty(), "either --kubeconfig1 or KUBECONFIG1 must be set") 74 | Expect(*kubeconfig2).ToNot(BeEmpty(), "either --kubeconfig2 or KUBECONFIG2 must be set") 75 | 76 | restcfg1, err := clientcmd.BuildConfigFromFlags("", *kubeconfig1) 77 | Expect(err).ToNot(HaveOccurred()) 78 | restcfg2, err := clientcmd.BuildConfigFromFlags("", *kubeconfig2) 79 | Expect(err).ToNot(HaveOccurred()) 80 | 81 | cluster1 = clusterClients{ 82 | k8s: kubernetes.NewForConfigOrDie(restcfg1), 83 | mcs: mcsclient.NewForConfigOrDie(restcfg1), 84 | } 85 | cluster2 = clusterClients{ 86 | k8s: kubernetes.NewForConfigOrDie(restcfg2), 87 | mcs: mcsclient.NewForConfigOrDie(restcfg2), 88 | } 89 | }) 90 | 91 | func execCmd(k8s kubernetes.Interface, config *restclient.Config, podName string, podNamespace string, command []string) ([]byte, []byte, error) { 92 | req := k8s.CoreV1().RESTClient().Post().Resource("pods").Name(podName).Namespace(podNamespace).SubResource("exec") 93 | req.VersionedParams(&v1.PodExecOptions{ 94 | Command: command, 95 | Stdin: false, 96 | Stdout: true, 97 | Stderr: true, 98 | TTY: true, 99 | }, scheme.ParameterCodec) 100 | exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) 101 | if err != nil { 102 | return []byte{}, []byte{}, err 103 | } 104 | var stdout, stderr bytes.Buffer 105 | err = exec.Stream(remotecommand.StreamOptions{ 106 | Stdin: nil, 107 | Stdout: &stdout, 108 | Stderr: &stderr, 109 | }) 110 | if err != nil { 111 | return []byte{}, []byte{}, err 112 | } 113 | return stdout.Bytes(), stderr.Bytes(), nil 114 | } 115 | 116 | func exportService(ctx context.Context, fromCluster, toCluster clusterClients, namespace string, svcName string) { 117 | _, err := fromCluster.mcs.MulticlusterV1alpha1().ServiceExports(namespace).Create(ctx, &v1alpha1.ServiceExport{ 118 | ObjectMeta: metav1.ObjectMeta{ 119 | Name: svcName, 120 | }, 121 | }, metav1.CreateOptions{}) 122 | Expect(err).ToNot(HaveOccurred()) 123 | var slices *discoveryv1.EndpointSliceList 124 | Eventually(func() int { 125 | eps := 0 126 | slices, err = fromCluster.k8s.DiscoveryV1().EndpointSlices(namespace).List(ctx, metav1.ListOptions{ 127 | LabelSelector: labels.Set{discoveryv1.LabelServiceName: svcName}.AsSelector().String(), 128 | }) 129 | Expect(err).ToNot(HaveOccurred()) 130 | for _, s := range slices.Items { 131 | eps += len(s.Endpoints) 132 | } 133 | return eps 134 | }, 30).Should(Equal(1)) 135 | importedSlice := slices.Items[0] // This direct indexing is ok because we just asserted above that there is exactly one element here 136 | importedSlice.ObjectMeta = metav1.ObjectMeta{ 137 | GenerateName: svcName + "-", 138 | Labels: map[string]string{ 139 | v1alpha1.LabelServiceName: svcName, 140 | }, 141 | } 142 | 143 | createdSlice, err := toCluster.k8s.DiscoveryV1().EndpointSlices(namespace).Create(ctx, &importedSlice, metav1.CreateOptions{}) 144 | Expect(err).ToNot(HaveOccurred()) 145 | 146 | Eventually(func() string { 147 | updatedSlice, err := toCluster.k8s.DiscoveryV1().EndpointSlices(namespace).Get(ctx, createdSlice.Name, metav1.GetOptions{}) 148 | Expect(err).ToNot(HaveOccurred()) 149 | return updatedSlice.Labels[discoveryv1.LabelServiceName] 150 | }).ShouldNot(BeEmpty()) 151 | } 152 | -------------------------------------------------------------------------------- /conformance/clusterip_service_dns.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package conformance 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "regexp" 23 | "strconv" 24 | "strings" 25 | 26 | . "github.com/onsi/ginkgo/v2" 27 | . "github.com/onsi/gomega" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 30 | ) 31 | 32 | var _ = Describe("", Label(OptionalLabel, DNSLabel, ClusterIPLabel), func() { 33 | t := newTestDriver() 34 | 35 | Specify("A DNS lookup of the ..svc.clusterset.local domain for a ClusterIP service should resolve to the "+ 36 | "clusterset IP", func() { 37 | AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") 38 | 39 | By("Retrieving ServiceImport") 40 | 41 | serviceImports := []*v1alpha1.ServiceImport{} 42 | for _, client := range clients { 43 | serviceImport := t.awaitServiceImport(&client, t.helloService.Name, false, 44 | func(g Gomega, serviceImport *v1alpha1.ServiceImport) { 45 | g.Expect(serviceImport.Spec.IPs).ToNot(BeEmpty(), "ServiceImport on cluster %q does not contain an IP", client.name) 46 | }) 47 | serviceImports = append(serviceImports, serviceImport) 48 | } 49 | 50 | command := []string{"sh", "-c", fmt.Sprintf("nslookup %s.%s.svc.clusterset.local", t.helloService.Name, t.namespace)} 51 | for i, client := range clients { 52 | clusterSetIP := serviceImports[i].Spec.IPs[0] 53 | By(fmt.Sprintf("Found ServiceImport on cluster %q with clusterset IP %q", client.name, clusterSetIP)) 54 | By(fmt.Sprintf("Executing command %q on cluster %q", strings.Join(command, " "), client.name)) 55 | 56 | t.awaitCmdOutputMatches(&client, command, clusterSetIP, 1, reportNonConformant("")) 57 | } 58 | }) 59 | 60 | Specify("A DNS SRV query of the ..svc.clusterset.local domain for a ClusterIP service should return valid SRV "+ 61 | "records", func() { 62 | AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") 63 | 64 | domainName := fmt.Sprintf("%s.%s.svc.clusterset.local", t.helloService.Name, t.namespace) 65 | 66 | for _, client := range clients { 67 | srvRecs := t.expectSRVRecords(&client, domainName) 68 | 69 | expSRVRecs := []srvRecord{{ 70 | port: t.helloService.Spec.Ports[0].Port, 71 | domainName: domainName, 72 | }} 73 | 74 | Expect(srvRecs).To(Equal(expSRVRecs), reportNonConformant( 75 | fmt.Sprintf("Received SRV records %v do not match the expected records %v", srvRecs, expSRVRecs))) 76 | } 77 | }) 78 | 79 | Specify("DNS lookups of the ..svc.cluster.local domain for a ClusterIP service should only resolve "+ 80 | "local services", func() { 81 | AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#dns") 82 | 83 | By(fmt.Sprintf("Retrieving local Service on cluster %q", clients[0].name)) 84 | 85 | var resolvedIP string 86 | 87 | Eventually(func() string { 88 | svc, err := clients[0].k8s.CoreV1().Services(t.namespace).Get(context.TODO(), t.helloService.Name, metav1.GetOptions{}) 89 | Expect(err).ToNot(HaveOccurred(), "Error retrieving the local Service") 90 | 91 | resolvedIP = svc.Spec.ClusterIP 92 | 93 | return resolvedIP 94 | }, 20, 1).ShouldNot(BeEmpty(), "The service was not assigned a cluster IP") 95 | 96 | By(fmt.Sprintf("Found local Service cluster IP %q", resolvedIP)) 97 | 98 | command := []string{"sh", "-c", fmt.Sprintf("nslookup %s.%s.svc.cluster.local", t.helloService.Name, t.namespace)} 99 | 100 | By(fmt.Sprintf("Executing command %q on cluster %q", strings.Join(command, " "), clients[0].name)) 101 | 102 | t.awaitCmdOutputMatches(&clients[0], command, resolvedIP, 1, reportNonConformant("")) 103 | }) 104 | }) 105 | 106 | func (t *testDriver) expectSRVRecords(c *clusterClients, domainName string) []srvRecord { 107 | command := []string{"sh", "-c", "nslookup -type=SRV " + domainName} 108 | 109 | By(fmt.Sprintf("Executing command %q on cluster %q", strings.Join(command, " "), c.name)) 110 | 111 | var srvRecs []srvRecord 112 | 113 | Eventually(func() []srvRecord { 114 | srvRecs = parseSRVRecords(t.execCmdOnRequestPod(c, command)) 115 | return srvRecs 116 | }, 20, 1).ShouldNot(BeEmpty(), reportNonConformant("")) 117 | 118 | return srvRecs 119 | } 120 | 121 | // Match SRV records from nslookup of the form: 122 | // 123 | // hello.mcs-conformance-1686874467.svc.clusterset.local service = 0 50 42 hello.mcs-conformance-1686874467.svc.clusterset.local 124 | // 125 | // to extract the port and target domain name (the last two tokens) 126 | var srvRecordRegEx = regexp.MustCompile(`.*=\s*\d*\s*\d*\s*(\d*)\s*([a-zA-Z0-9-.]*)`) 127 | 128 | type srvRecord struct { 129 | port int32 130 | domainName string 131 | } 132 | 133 | func (s srvRecord) String() string { 134 | return fmt.Sprintf("port:%d, domainName:%q", s.port, s.domainName) 135 | } 136 | 137 | func parseSRVRecords(str string) []srvRecord { 138 | var recs []srvRecord 139 | 140 | matches := srvRecordRegEx.FindAllStringSubmatch(str, -1) 141 | for i := range matches { 142 | // First match at index 0 is the full text that was matched; index 1 is the port and index 2 is the domain name. 143 | port, _ := strconv.ParseInt(matches[i][1], 10, 32) 144 | recs = append(recs, srvRecord{ 145 | port: int32(port), 146 | domainName: matches[i][2], 147 | }) 148 | } 149 | 150 | return recs 151 | } 152 | -------------------------------------------------------------------------------- /controllers/serviceimport_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math/rand" 23 | "time" 24 | 25 | . "github.com/onsi/ginkgo/v2" 26 | . "github.com/onsi/gomega" 27 | v1 "k8s.io/api/core/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/apimachinery/pkg/types" 30 | "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 31 | ) 32 | 33 | var _ = Describe("ServiceImport", func() { 34 | var ( 35 | serviceImport v1alpha1.ServiceImport 36 | serviceName types.NamespacedName 37 | derivedServiceName types.NamespacedName 38 | ) 39 | ctx := context.Background() 40 | Context("should be ignored", func() { 41 | Specify("when headless", func() { 42 | Expect(shouldIgnoreImport(&v1alpha1.ServiceImport{ 43 | ObjectMeta: metav1.ObjectMeta{ 44 | Namespace: testNS, 45 | Name: "headless", 46 | }, 47 | Spec: v1alpha1.ServiceImportSpec{ 48 | Type: v1alpha1.Headless, 49 | Ports: []v1alpha1.ServicePort{ 50 | {Port: 80}, 51 | }, 52 | }, 53 | })).To(BeTrue()) 54 | }) 55 | Specify("when deleted", func() { 56 | Expect(shouldIgnoreImport(&v1alpha1.ServiceImport{ 57 | ObjectMeta: metav1.ObjectMeta{ 58 | Namespace: testNS, 59 | Name: "deleted", 60 | DeletionTimestamp: &metav1.Time{Time: time.Now()}, 61 | }, 62 | Spec: v1alpha1.ServiceImportSpec{ 63 | Type: v1alpha1.ClusterSetIP, 64 | Ports: []v1alpha1.ServicePort{ 65 | {Port: 80}, 66 | }, 67 | }, 68 | })).To(BeTrue()) 69 | }) 70 | }) 71 | Context("created", func() { 72 | BeforeEach(func() { 73 | serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} 74 | derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} 75 | serviceImport = v1alpha1.ServiceImport{ 76 | ObjectMeta: metav1.ObjectMeta{ 77 | Namespace: testNS, 78 | Name: serviceName.Name, 79 | }, 80 | Spec: v1alpha1.ServiceImportSpec{ 81 | Type: v1alpha1.ClusterSetIP, 82 | Ports: []v1alpha1.ServicePort{ 83 | {Port: 80}, 84 | }, 85 | }, 86 | } 87 | Expect(k8s.Create(ctx, &serviceImport)).To(Succeed()) 88 | }) 89 | It("has derived service annotation", func() { 90 | Eventually(func() string { 91 | var s v1alpha1.ServiceImport 92 | Expect(k8s.Get(ctx, serviceName, &s)).To(Succeed()) 93 | return s.Annotations[DerivedServiceAnnotation] 94 | }, 10).Should(Equal(derivedName(serviceName))) 95 | }, 10) 96 | It("has derived service IP", func() { 97 | var s v1alpha1.ServiceImport 98 | Eventually(func() string { 99 | Expect(k8s.Get(ctx, serviceName, &s)).To(Succeed()) 100 | if len(s.Spec.IPs) > 0 { 101 | return s.Spec.IPs[0] 102 | } 103 | return "" 104 | }, 10).ShouldNot(BeEmpty()) 105 | }, 15) 106 | It("created derived service", func() { 107 | var s v1.Service 108 | Eventually(func() error { 109 | return k8s.Get(ctx, derivedServiceName, &s) 110 | }, 10).Should(Succeed()) 111 | Expect(len(s.OwnerReferences)).To(Equal(1)) 112 | Expect(s.OwnerReferences[0].UID).To(Equal(serviceImport.UID)) 113 | }, 15) 114 | It("removes derived service", func() { 115 | var s v1.Service 116 | Eventually(func() error { 117 | return k8s.Get(ctx, derivedServiceName, &s) 118 | }, 10).Should(Succeed()) 119 | var imp v1alpha1.ServiceImport 120 | Expect(k8s.Get(ctx, serviceName, &imp)).To(Succeed()) 121 | Expect(k8s.Delete(ctx, &imp)).To(Succeed()) 122 | Eventually(func() error { 123 | return k8s.Get(ctx, derivedServiceName, &s) 124 | }, 15).ShouldNot(Succeed()) 125 | }, 15) 126 | }) 127 | Context("created with IP", func() { 128 | BeforeEach(func() { 129 | serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} 130 | derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} 131 | serviceImport = v1alpha1.ServiceImport{ 132 | ObjectMeta: metav1.ObjectMeta{ 133 | Namespace: testNS, 134 | Name: serviceName.Name, 135 | }, 136 | Spec: v1alpha1.ServiceImportSpec{ 137 | Type: v1alpha1.ClusterSetIP, 138 | Ports: []v1alpha1.ServicePort{ 139 | {Port: 80}, 140 | }, 141 | }, 142 | } 143 | Expect(k8s.Create(ctx, &serviceImport)).To(Succeed()) 144 | }) 145 | It("updates derived service IP", func() { 146 | var svcImport v1alpha1.ServiceImport 147 | var s v1.Service 148 | Eventually(func() error { 149 | return k8s.Get(ctx, derivedServiceName, &s) 150 | }, 10).Should(Succeed()) 151 | Eventually(func() string { 152 | Expect(k8s.Get(ctx, serviceName, &svcImport)).To(Succeed()) 153 | if len(svcImport.Spec.IPs) > 0 { 154 | return svcImport.Spec.IPs[0] 155 | } 156 | return "" 157 | }, 10).Should(Equal(s.Spec.ClusterIP)) 158 | }, 15) 159 | }) 160 | Context("created with existing clustersetIP", func() { 161 | BeforeEach(func() { 162 | serviceName = types.NamespacedName{Namespace: testNS, Name: fmt.Sprintf("svc-%v", rand.Uint64())} 163 | derivedServiceName = types.NamespacedName{Namespace: testNS, Name: derivedName(serviceName)} 164 | serviceImport = v1alpha1.ServiceImport{ 165 | ObjectMeta: metav1.ObjectMeta{ 166 | Namespace: testNS, 167 | Name: serviceName.Name, 168 | }, 169 | Spec: v1alpha1.ServiceImportSpec{ 170 | Type: v1alpha1.ClusterSetIP, 171 | Ports: []v1alpha1.ServicePort{ 172 | {Port: 80}, 173 | }, 174 | IPs: []string{"10.42.42.42"}, 175 | }, 176 | } 177 | Expect(k8s.Create(ctx, &serviceImport)).To(Succeed()) 178 | }) 179 | It("updates service loadbalancer status with service import IPs", func() { 180 | var svcImport v1alpha1.ServiceImport 181 | var s v1.Service 182 | Eventually(func() error { 183 | return k8s.Get(ctx, derivedServiceName, &s) 184 | }, 10).Should(Succeed()) 185 | Eventually(func() string { 186 | Expect(k8s.Get(ctx, serviceName, &svcImport)).To(Succeed()) 187 | if len(svcImport.Spec.IPs) > 0 { 188 | return svcImport.Spec.IPs[0] 189 | } 190 | return "" 191 | }, 10).Should(Equal(s.Status.LoadBalancer.Ingress[0].IP)) 192 | }, 15) 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2015 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from __future__ import print_function 18 | 19 | import argparse 20 | import difflib 21 | import glob 22 | import json 23 | import mmap 24 | import os 25 | import re 26 | import sys 27 | from datetime import date 28 | 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument( 31 | "filenames", 32 | help="list of files to check, all files if unspecified", 33 | nargs='*') 34 | 35 | rootdir = os.path.dirname(__file__) + "/../../" 36 | rootdir = os.path.abspath(rootdir) 37 | parser.add_argument( 38 | "--rootdir", default=rootdir, help="root directory to examine") 39 | 40 | default_boilerplate_dir = os.path.join(rootdir, "hack/boilerplate") 41 | parser.add_argument( 42 | "--boilerplate-dir", default=default_boilerplate_dir) 43 | 44 | parser.add_argument( 45 | "-v", "--verbose", 46 | help="give verbose output regarding why a file does not pass", 47 | action="store_true") 48 | 49 | args = parser.parse_args() 50 | 51 | verbose_out = sys.stderr if args.verbose else open("/dev/null", "w") 52 | 53 | 54 | def get_refs(): 55 | refs = {} 56 | 57 | for path in glob.glob(os.path.join(args.boilerplate_dir, "boilerplate.*.txt")): 58 | extension = os.path.basename(path).split(".")[1] 59 | 60 | ref_file = open(path, 'r') 61 | ref = ref_file.read().splitlines() 62 | ref_file.close() 63 | refs[extension] = ref 64 | 65 | return refs 66 | 67 | 68 | def file_passes(filename, refs, regexs): 69 | try: 70 | f = open(filename, 'r') 71 | except Exception as exc: 72 | print("Unable to open %s: %s" % (filename, exc), file=verbose_out) 73 | return False 74 | 75 | data = f.read() 76 | f.close() 77 | 78 | basename = os.path.basename(filename) 79 | extension = file_extension(filename) 80 | if extension != "": 81 | ref = refs[extension] 82 | else: 83 | ref = refs[basename] 84 | 85 | # remove build tags from the top of Go files 86 | if extension == "go": 87 | p = regexs["go_build_constraints"] 88 | (data, found) = p.subn("", data, 1) 89 | 90 | # remove shebang from the top of shell files 91 | if extension == "sh": 92 | p = regexs["shebang"] 93 | (data, found) = p.subn("", data, 1) 94 | 95 | data = data.splitlines() 96 | 97 | # if our test file is smaller than the reference it surely fails! 98 | if len(ref) > len(data): 99 | print('File %s smaller than reference (%d < %d)' % 100 | (filename, len(data), len(ref)), 101 | file=verbose_out) 102 | return False 103 | 104 | # trim our file to the same number of lines as the reference file 105 | data = data[:len(ref)] 106 | 107 | p = regexs["year"] 108 | for d in data: 109 | if p.search(d): 110 | print('File %s is missing the year' % filename, file=verbose_out) 111 | return False 112 | 113 | # Replace all occurrences of the regex "CURRENT_YEAR|...|2016|2015|2014" with "YEAR" 114 | p = regexs["date"] 115 | for i, d in enumerate(data): 116 | (data[i], found) = p.subn('YEAR', d) 117 | if found != 0: 118 | break 119 | 120 | # if we don't match the reference at this point, fail 121 | if ref != data: 122 | print("Header in %s does not match reference, diff:" % 123 | filename, file=verbose_out) 124 | if args.verbose: 125 | print(file=verbose_out) 126 | for line in difflib.unified_diff(ref, data, 'reference', filename, lineterm=''): 127 | print(line, file=verbose_out) 128 | print(file=verbose_out) 129 | return False 130 | 131 | return True 132 | 133 | 134 | def file_extension(filename): 135 | return os.path.splitext(filename)[1].split(".")[-1].lower() 136 | 137 | 138 | skipped_dirs = [ 139 | '.git', 140 | "vendor", 141 | "test/e2e/framework/framework.go", 142 | "images" 143 | ] 144 | 145 | 146 | def normalize_files(files): 147 | newfiles = [] 148 | for pathname in files: 149 | if any(x in pathname for x in skipped_dirs): 150 | continue 151 | newfiles.append(pathname) 152 | for i, pathname in enumerate(newfiles): 153 | if not os.path.isabs(pathname): 154 | newfiles[i] = os.path.join(args.rootdir, pathname) 155 | return newfiles 156 | 157 | 158 | def get_files(extensions): 159 | files = [] 160 | if len(args.filenames) > 0: 161 | files = args.filenames 162 | else: 163 | for root, dirs, walkfiles in os.walk(args.rootdir): 164 | # don't visit certain dirs. This is just a performance improvement 165 | # as we would prune these later in normalize_files(). But doing it 166 | # cuts down the amount of filesystem walking we do and cuts down 167 | # the size of the file list 168 | for d in skipped_dirs: 169 | if d in dirs: 170 | dirs.remove(d) 171 | 172 | for name in walkfiles: 173 | pathname = os.path.join(root, name) 174 | files.append(pathname) 175 | 176 | files = normalize_files(files) 177 | outfiles = [] 178 | for pathname in files: 179 | basename = os.path.basename(pathname) 180 | extension = file_extension(pathname) 181 | if extension in extensions or basename in extensions: 182 | outfiles.append(pathname) 183 | return outfiles 184 | 185 | 186 | def get_regexs(): 187 | regexs = {} 188 | # Search for "YEAR" which exists in the boilerplate, but shouldn't in the real thing 189 | regexs["year"] = re.compile('YEAR') 190 | # dates can be 2014, 2015, 2016, ..., CURRENT_YEAR, company holder names can be anything 191 | years = range(2014, date.today().year + 1) 192 | regexs["date"] = re.compile( 193 | '(%s)' % "|".join(map(lambda l: str(l), years))) 194 | # strip // +build \n\n build constraints 195 | regexs["go_build_constraints"] = re.compile( 196 | r"^(// ?(go:|\+)build.*\n)+\n", re.MULTILINE) 197 | # strip #!.* from shell scripts 198 | regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE) 199 | return regexs 200 | 201 | 202 | def main(): 203 | regexs = get_regexs() 204 | refs = get_refs() 205 | filenames = get_files(refs.keys()) 206 | 207 | for filename in filenames: 208 | if not file_passes(filename, refs, regexs): 209 | print(filename, file=sys.stdout) 210 | 211 | return 0 212 | 213 | 214 | if __name__ == "__main__": 215 | sys.exit(main()) 216 | -------------------------------------------------------------------------------- /config/crd/multicluster.x-k8s.io_serviceexports.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: apiextensions.k8s.io/v1 15 | kind: CustomResourceDefinition 16 | metadata: 17 | name: serviceexports.multicluster.x-k8s.io 18 | labels: 19 | multicluster.x-k8s.io/release-version: "v0.3.0" 20 | # The revision is updated on each CRD change and reset back to 0 on every new version. 21 | # It can be used together with the version label when installing those CRDs 22 | # and prevent any downgrades. 23 | multicluster.x-k8s.io/crd-schema-revision: "1" 24 | spec: 25 | group: multicluster.x-k8s.io 26 | scope: Namespaced 27 | names: 28 | plural: serviceexports 29 | singular: serviceexport 30 | kind: ServiceExport 31 | shortNames: 32 | - svcex 33 | - svcexport 34 | versions: 35 | - name: v1alpha1 36 | served: true 37 | storage: true 38 | subresources: 39 | status: {} 40 | additionalPrinterColumns: 41 | - name: Age 42 | type: date 43 | jsonPath: .metadata.creationTimestamp 44 | "schema": 45 | "openAPIV3Schema": 46 | description: |- 47 | ServiceExport declares that the Service with the same name and namespace 48 | as this export should be consumable from other clusters. 49 | type: object 50 | properties: 51 | apiVersion: 52 | description: |- 53 | APIVersion defines the versioned schema of this representation of an object. 54 | Servers should convert recognized schemas to the latest internal value, and 55 | may reject unrecognized values. 56 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 57 | type: string 58 | kind: 59 | description: |- 60 | Kind is a string value representing the REST resource this object represents. 61 | Servers may infer this from the endpoint the client submits requests to. 62 | Cannot be updated. 63 | In CamelCase. 64 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 65 | type: string 66 | metadata: 67 | type: object 68 | spec: 69 | description: spec defines the behavior of a ServiceExport. 70 | type: object 71 | properties: 72 | exportedAnnotations: 73 | description: exportedAnnotations describes the annotations exported. It is optional for implementation. 74 | type: object 75 | additionalProperties: 76 | type: string 77 | exportedLabels: 78 | description: exportedLabels describes the labels exported. It is optional for implementation. 79 | type: object 80 | additionalProperties: 81 | type: string 82 | status: 83 | description: |- 84 | status describes the current state of an exported service. 85 | Service configuration comes from the Service that had the same 86 | name and namespace as this ServiceExport. 87 | Populated by the multi-cluster service implementation's controller. 88 | type: object 89 | properties: 90 | conditions: 91 | type: array 92 | items: 93 | description: Condition contains details for one aspect of the current state of this API Resource. 94 | type: object 95 | required: 96 | - lastTransitionTime 97 | - message 98 | - reason 99 | - status 100 | - type 101 | properties: 102 | lastTransitionTime: 103 | description: |- 104 | lastTransitionTime is the last time the condition transitioned from one status to another. 105 | This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 106 | type: string 107 | format: date-time 108 | message: 109 | description: |- 110 | message is a human readable message indicating details about the transition. 111 | This may be an empty string. 112 | type: string 113 | maxLength: 32768 114 | observedGeneration: 115 | description: |- 116 | observedGeneration represents the .metadata.generation that the condition was set based upon. 117 | For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 118 | with respect to the current state of the instance. 119 | type: integer 120 | format: int64 121 | minimum: 0 122 | reason: 123 | description: |- 124 | reason contains a programmatic identifier indicating the reason for the condition's last transition. 125 | Producers of specific condition types may define expected values and meanings for this field, 126 | and whether the values are considered a guaranteed API. 127 | The value should be a CamelCase string. 128 | This field may not be empty. 129 | type: string 130 | maxLength: 1024 131 | minLength: 1 132 | pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 133 | status: 134 | description: status of the condition, one of True, False, Unknown. 135 | type: string 136 | enum: 137 | - "True" 138 | - "False" 139 | - Unknown 140 | type: 141 | description: type of condition in CamelCase or in foo.example.com/CamelCase. 142 | type: string 143 | maxLength: 316 144 | 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])$ 145 | x-kubernetes-list-map-keys: 146 | - type 147 | x-kubernetes-list-type: map 148 | -------------------------------------------------------------------------------- /e2e/localserviceimpact_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2etest 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math/rand" 23 | "strings" 24 | "time" 25 | 26 | . "github.com/onsi/ginkgo/v2" 27 | . "github.com/onsi/gomega" 28 | appsv1 "k8s.io/api/apps/v1" 29 | v1 "k8s.io/api/core/v1" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | ) 32 | 33 | var _ = Describe("Local service not impacted", func() { 34 | helloDeployment := appsv1.Deployment{ 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Name: "hello", 37 | }, 38 | Spec: appsv1.DeploymentSpec{ 39 | Replicas: &replicaCount, 40 | Selector: &metav1.LabelSelector{ 41 | MatchLabels: map[string]string{ 42 | "app": "hello", 43 | }, 44 | }, 45 | Template: v1.PodTemplateSpec{ 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Labels: map[string]string{"app": "hello"}, 48 | }, 49 | Spec: v1.PodSpec{ 50 | Containers: []v1.Container{ 51 | { 52 | Name: "hello-tcp", 53 | Image: "alpine/socat:1.7.4.4", 54 | Args: []string{"-v", "-v", "TCP-LISTEN:42,crlf,reuseaddr,fork", "SYSTEM:echo $(CLUSTER_ID)"}, 55 | Env: []v1.EnvVar{ 56 | { 57 | Name: "CLUSTER_ID", 58 | ValueFrom: &v1.EnvVarSource{ 59 | ConfigMapKeyRef: &v1.ConfigMapKeySelector{ 60 | LocalObjectReference: v1.LocalObjectReference{ 61 | Name: "cluster-info", 62 | }, 63 | Key: "clusterID", 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }, 71 | }, 72 | }, 73 | } 74 | 75 | var ( 76 | namespace string 77 | 78 | ctx = context.Background() 79 | reqPod *v1.Pod 80 | ) 81 | 82 | BeforeEach(func() { 83 | namespace = fmt.Sprintf("mcse2e-conformance-%v", rand.Uint32()) 84 | _, err := cluster1.k8s.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ 85 | ObjectMeta: metav1.ObjectMeta{Name: namespace}, 86 | }, metav1.CreateOptions{}) 87 | Expect(err).ToNot(HaveOccurred()) 88 | _, err = cluster2.k8s.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ 89 | ObjectMeta: metav1.ObjectMeta{Name: namespace}, 90 | }, metav1.CreateOptions{}) 91 | Expect(err).ToNot(HaveOccurred()) 92 | _, err = cluster1.k8s.CoreV1().ConfigMaps(namespace).Create(ctx, &v1.ConfigMap{ 93 | ObjectMeta: metav1.ObjectMeta{ 94 | Name: "cluster-info", 95 | }, 96 | Data: map[string]string{ 97 | "clusterID": "cluster1", 98 | }, 99 | }, metav1.CreateOptions{}) 100 | Expect(err).NotTo(HaveOccurred()) 101 | _, err = cluster2.k8s.CoreV1().ConfigMaps(namespace).Create(ctx, &v1.ConfigMap{ 102 | ObjectMeta: metav1.ObjectMeta{ 103 | Name: "cluster-info", 104 | }, 105 | Data: map[string]string{ 106 | "clusterID": "cluster2", 107 | }, 108 | }, metav1.CreateOptions{}) 109 | Expect(err).NotTo(HaveOccurred()) 110 | pod := requestPod 111 | _, err = cluster1.k8s.CoreV1().Pods(namespace).Create(ctx, &pod, metav1.CreateOptions{}) 112 | Expect(err).ToNot(HaveOccurred()) 113 | dep := helloDeployment 114 | _, err = cluster1.k8s.AppsV1().Deployments(namespace).Create(ctx, &dep, metav1.CreateOptions{}) 115 | Expect(err).ToNot(HaveOccurred()) 116 | _, err = cluster2.k8s.AppsV1().Deployments(namespace).Create(ctx, &dep, metav1.CreateOptions{}) 117 | Expect(err).ToNot(HaveOccurred()) 118 | svc := helloService 119 | _, err = cluster1.k8s.CoreV1().Services(namespace).Create(ctx, &svc, metav1.CreateOptions{}) 120 | Expect(err).ToNot(HaveOccurred()) 121 | _, err = cluster2.k8s.CoreV1().Services(namespace).Create(ctx, &svc, metav1.CreateOptions{}) 122 | Expect(err).ToNot(HaveOccurred()) 123 | _, err = cluster1.mcs.MulticlusterV1alpha1().ServiceImports(namespace).Create(ctx, &helloServiceImport, metav1.CreateOptions{}) 124 | Expect(err).ToNot(HaveOccurred()) 125 | deploymentAvailable := func(clients clusterClients) func(Gomega) { 126 | return func(g Gomega) { 127 | deployment, err := clients.k8s.AppsV1().Deployments(namespace).Get(ctx, dep.Name, metav1.GetOptions{}) 128 | g.Expect(err).NotTo(HaveOccurred()) 129 | g.Expect(deployment.Status.Conditions).To(ContainElement(Satisfy(func(cond appsv1.DeploymentCondition) bool { 130 | return cond.Type == appsv1.DeploymentAvailable && 131 | cond.Status == v1.ConditionTrue 132 | }))) 133 | } 134 | } 135 | Eventually(deploymentAvailable(cluster1), 30).Should(Succeed()) 136 | Eventually(deploymentAvailable(cluster2), 30).Should(Succeed()) 137 | 138 | exportService(ctx, cluster2, cluster1, namespace, svc.Name) 139 | 140 | Eventually(func() []string { 141 | svcImport, err := cluster1.mcs.MulticlusterV1alpha1().ServiceImports(namespace).Get(ctx, helloServiceImport.Name, metav1.GetOptions{}) 142 | Expect(err).ToNot(HaveOccurred()) 143 | return svcImport.Spec.IPs 144 | }).ShouldNot(BeEmpty()) 145 | reqPod, err = cluster1.k8s.CoreV1().Pods(namespace).Get(ctx, requestPod.Name, metav1.GetOptions{}) 146 | Expect(err).ToNot(HaveOccurred()) 147 | By("Created all in " + namespace) 148 | }) 149 | AfterEach(func() { 150 | if *noTearDown { 151 | By(fmt.Sprintf("Skipping teardown. Test namespace %q", namespace)) 152 | By(fmt.Sprintf("Cluster 1: kubectl --kubeconfig %q -n %q", *kubeconfig1, namespace)) 153 | By(fmt.Sprintf("Cluster 2: kubectl --kubeconfig %q -n %q", *kubeconfig2, namespace)) 154 | return 155 | } 156 | Expect(cluster1.k8s.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{})).To(Succeed()) 157 | Expect(cluster2.k8s.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{})).To(Succeed()) 158 | }) 159 | Specify("DNS resolves as expected", func() { 160 | checkAllClustersReachable := func(command []string, clusterIDs ...string) { 161 | clusters := map[string]int{} 162 | Eventually(func(g Gomega) { 163 | stdout, _, err := execCmd(cluster1.k8s, restcfg1, reqPod.Name, reqPod.Namespace, command) 164 | g.Expect(err).ToNot(HaveOccurred()) 165 | clusterID := strings.TrimSpace(string(stdout)) 166 | g.Expect(clusterID).To(BeElementOf(clusterIDs)) 167 | clusters[clusterID]++ 168 | }).MustPassRepeatedly(20).Within(time.Second * 10).Should(Succeed()) 169 | Expect(clusters).To(HaveEach(Not(BeZero()))) 170 | } 171 | 172 | By("verifying a local, unexported service is reachable via cluster.local") 173 | command := []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.cluster.local 42", helloService.Name, namespace)} 174 | checkAllClustersReachable(command, "cluster1") 175 | 176 | By("verifying a remote, exported service is reachable via clusterset.local") 177 | command = []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.clusterset.local 42", helloServiceImport.Name, namespace)} 178 | checkAllClustersReachable(command, "cluster2") 179 | 180 | By("exporting the service from cluster1") 181 | exportService(ctx, cluster1, cluster1, namespace, helloService.Name) 182 | 183 | By("verifying a service exported from both local and remote clusters is reachable via clusterset.local") 184 | checkAllClustersReachable(command, "cluster1", "cluster2") 185 | 186 | By("verifying a local, exported service is reachable via cluster.local") 187 | command = []string{"sh", "-c", fmt.Sprintf("echo hi | nc %s.%s.svc.cluster.local 42", helloService.Name, namespace)} 188 | checkAllClustersReachable(command, "cluster1") 189 | }) 190 | }) 191 | --------------------------------------------------------------------------------