diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2048a3c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: false + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.64.5 + args: --timeout 5m0s + + verify: + name: Verify + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: false + + - name: Verify + run: hack/verify-all.sh -v + + test: + name: Unit Test + runs-on: ubuntu-latest + needs: verify + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: false + + - name: Run tests + run: make test diff --git a/.golangci.yml b/.golangci.yml index b0993d7..29a349e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,7 +20,7 @@ linters: enable: - dupl - errcheck - - exportloopref + - copyloopvar - goconst - gocyclo - gofmt diff --git a/Makefile b/Makefile index 8d0768f..ef5dfe1 100644 --- a/Makefile +++ b/Makefile @@ -47,11 +47,11 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./apis/..." paths="./pkg/..." paths="./cmd/..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./apis/..." paths="./pkg/..." paths="./cmd/..." hack/update-codegen.sh .PHONY: fmt @@ -164,7 +164,7 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) KUSTOMIZE_VERSION ?= v5.3.0 CONTROLLER_TOOLS_VERSION ?= v0.17.3 ENVTEST_VERSION ?= release-0.17 -GOLANGCI_LINT_VERSION ?= v1.54.2 +GOLANGCI_LINT_VERSION ?= v1.64.5 KB_TOOLS_ARCHIVE_NAME :=kubebuilder-tools-$(ENVTEST_K8S_VERSION)-$(GOHOSTOS)-$(GOHOSTARCH).tar.gz KB_TOOLS_ARCHIVE_PATH := /tmp/$(KB_TOOLS_ARCHIVE_NAME) export KUBEBUILDER_ASSETS ?=/tmp/kubebuilder/bin @@ -187,7 +187,12 @@ $(ENVTEST): $(LOCALBIN) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) - $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) + @[ -f $(GOLANGCI_LINT) ] || { \ + set -e; \ + echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)" ;\ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LOCALBIN) $(GOLANGCI_LINT_VERSION) ;\ + mv $(LOCALBIN)/golangci-lint $(GOLANGCI_LINT) ;\ + } # download the kubebuilder-tools to get kube-apiserver binaries from it .PHONY: ensure-kubebuilder-tools diff --git a/README.md b/README.md index 2f708e0..bd254ef 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ and this repository serves as the foundation for developing a standardized, robust framework for multi-cluster management in a cloud-native environment. The Cluster Inventory API aims to provide a consistent and automated approach for applications, -frameworks, and toolsets to discover and interact with multiple Kubernetes clusters. +frameworks, and toolsets to discover, interact with, and make placement decisions across multiple Kubernetes clusters. The concept of Cluster Inventory is akin to service discovery in a microservices architecture. It allows multi-cluster applications to dynamically discover available clusters and respond to various cluster lifecycle events. Such events include auto-scaling, upgrades, failures, and connectivity issues. @@ -35,6 +35,26 @@ can integrate without needing to navigate the specific implementation details of - **Vendor Neutrality**: The API provides a vendor-neutral integration point, allowing operations tools and external consumers to define and manage clusters across different cloud environments uniformly. +## PlacementDecision API + +The [PlacementDecision API](https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/5313-placement-decision/README.md) +is a vendor-neutral API that standardizes the output of multicluster placement calculations. +A `PlacementDecision` object is data only: a namespaced list of chosen clusters whose referenced names +must map one-to-one to `ClusterProfile`s. Any scheduler can emit the object and any consumer can watch it. + +### Motivation and Goals + +Today every multicluster scheduler publishes its own API to convey where a workload should run, +forcing downstream tools such as GitOps engines, workload orchestrators, progressive rollout controllers, +or AI/ML pipelines to understand a scheduler-specific API. + +The `PlacementDecision API` solves the integration explosion problem: + +- **Reduces integration burden**: Consumers write ONE integration that works with ANY scheduler implementing this API. +- **Enables scheduler portability**: Switch schedulers without rewriting consumer code. +- **Enables consumer portability**: New consumers work with all schedulers by implementing one standard API. +- **Simplifies RBAC**: One resource schema to secure instead of different permissions for each scheduler's API. + ## Community, discussion, contribution, and support Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). diff --git a/apis/v1alpha1/groupversion_info.go b/apis/v1alpha1/groupversion_info.go index de21240..291f2d2 100644 --- a/apis/v1alpha1/groupversion_info.go +++ b/apis/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,8 +20,6 @@ limitations under the License. package v1alpha1 import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) @@ -31,9 +29,16 @@ const ( Group = "multicluster.x-k8s.io" // Version is the API version. Version = "v1alpha1" - // Kind is the resource kind. + + // ClusterProfile resource constants. + // Kind is the resource kind for ClusterProfile. Kind = "ClusterProfile" resource = "clusterprofiles" + + // PlacementDecision resource constants. + // PlacementDecisionKind is the resource kind for PlacementDecision. + PlacementDecisionKind = "PlacementDecision" + placementDecisionResource = "placementdecisions" ) var ( @@ -61,6 +66,20 @@ var ( Resource: resource, } + // PlacementDecisionSchemeGroupVersionKind is the group, version and kind for the PlacementDecision CR. + PlacementDecisionSchemeGroupVersionKind = schema.GroupVersionKind{ + Group: Group, + Version: Version, + Kind: PlacementDecisionKind, + } + + // PlacementDecisionSchemeGroupVersionResource is the group, version and resource for the PlacementDecision CR. + PlacementDecisionSchemeGroupVersionResource = schema.GroupVersionResource{ + Group: Group, + Version: Version, + Resource: placementDecisionResource, + } + // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) @@ -70,13 +89,3 @@ var ( func Resource(resource string) schema.GroupResource { return schema.GroupResource{Group: GroupVersion.Group, Resource: resource} } - -// Adds the list of known types to api.Scheme. -func addKnownTypes(scheme *runtime.Scheme) error { - scheme.AddKnownTypes(GroupVersion, - &ClusterProfile{}, - &ClusterProfileList{}, - ) - metav1.AddToGroupVersion(scheme, GroupVersion) - return nil -} diff --git a/apis/v1alpha1/placementdecision_types.go b/apis/v1alpha1/placementdecision_types.go new file mode 100644 index 0000000..55a104b --- /dev/null +++ b/apis/v1alpha1/placementdecision_types.go @@ -0,0 +1,96 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Label keys used for PlacementDecision correlation and discovery. +const ( + // DecisionKeyLabel links all slices to the same decision when a decision spans multiple slices. + // When multiple slices exist for one logical decision, the producer MUST set the same + // decision-key on all slices. + DecisionKeyLabel = "multicluster.x-k8s.io/decision-key" + + // DecisionIndexLabel indicates the index position of this slice when order matters. + // If a scheduler needs to preserve the order of selected clusters and the result spans + // multiple slices, it should label each PlacementDecision with this label where the + // value starts at 0 and increments by 1. + DecisionIndexLabel = "multicluster.x-k8s.io/decision-index" + + // PlacementKeyLabel links a decision to an originating workload when applicable. + // Producers may set this label on PlacementDecision slices when the decision is workload scoped. + // Decisions not tied to a workload need not set this label. + PlacementKeyLabel = "multicluster.x-k8s.io/placement-key" +) + +// ClusterDecision references a target ClusterProfile to apply workloads to. +type ClusterDecision struct { + // ClusterProfileRef is a reference to the target ClusterProfile. + // The reference must point to a valid ClusterProfile in the fleet. + // +required + ClusterProfileRef corev1.ObjectReference `json:"clusterProfileRef"` + + // Reason is an optional explanation of why this cluster was chosen. + // This can be useful for debugging and auditing placement decisions. + // +optional + Reason string `json:"reason,omitempty"` +} + +//+genclient +//+kubebuilder:object:root=true +//+kubebuilder:resource:scope=Namespaced + +// PlacementDecision publishes the set of clusters chosen by a scheduler at a point in time. +// It is a data-only resource that acts as the interface between schedulers and consumers. +// Schedulers write decisions using this format; consumers read from it. +// +// Following the EndpointSlice convention, a single scheduling decision can fan out to N +// PlacementDecision slices, each limited to 100 clusters. To correlate slices, producers +// MUST set the same multicluster.x-k8s.io/decision-key label on all slices when more than +// one slice exists. +type PlacementDecision struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Decisions is the list of clusters chosen for this placement decision. + // Up to 100 ClusterDecisions per object (slice) to stay well below the etcd limit. + // +kubebuilder:validation:MinItems=0 + // +kubebuilder:validation:MaxItems=100 + // +required + Decisions []ClusterDecision `json:"decisions"` + + // SchedulerName is the name of the scheduler that created this decision. + // This is optional and can be used for debugging and auditing purposes. + // +optional + SchedulerName string `json:"schedulerName,omitempty"` +} + +//+kubebuilder:object:root=true + +// PlacementDecisionList contains a list of PlacementDecision. +type PlacementDecisionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PlacementDecision `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PlacementDecision{}, &PlacementDecisionList{}) +} diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 339bd56..88fd69d 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ //go:build !ignore_autogenerated /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ package v1alpha1 import ( "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -41,6 +41,22 @@ func (in *AccessProvider) DeepCopy() *AccessProvider { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterDecision) DeepCopyInto(out *ClusterDecision) { + *out = *in + out.ClusterProfileRef = in.ClusterProfileRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterDecision. +func (in *ClusterDecision) DeepCopy() *ClusterDecision { + if in == nil { + return nil + } + out := new(ClusterDecision) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterManager) DeepCopyInto(out *ClusterManager) { *out = *in @@ -190,6 +206,68 @@ func (in *ClusterVersion) DeepCopy() *ClusterVersion { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementDecision) DeepCopyInto(out *PlacementDecision) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Decisions != nil { + in, out := &in.Decisions, &out.Decisions + *out = make([]ClusterDecision, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementDecision. +func (in *PlacementDecision) DeepCopy() *PlacementDecision { + if in == nil { + return nil + } + out := new(PlacementDecision) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlacementDecision) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementDecisionList) DeepCopyInto(out *PlacementDecisionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PlacementDecision, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementDecisionList. +func (in *PlacementDecisionList) DeepCopy() *PlacementDecisionList { + if in == nil { + return nil + } + out := new(PlacementDecisionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlacementDecisionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Property) DeepCopyInto(out *Property) { *out = *in diff --git a/client/clientset/versioned/clientset.go b/client/clientset/versioned/clientset.go index 94928f1..523b931 100644 --- a/client/clientset/versioned/clientset.go +++ b/client/clientset/versioned/clientset.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/fake/clientset_generated.go b/client/clientset/versioned/fake/clientset_generated.go index 99f305a..eb40e5c 100644 --- a/client/clientset/versioned/fake/clientset_generated.go +++ b/client/clientset/versioned/fake/clientset_generated.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/fake/doc.go b/client/clientset/versioned/fake/doc.go index 634bd02..0f3cdf2 100644 --- a/client/clientset/versioned/fake/doc.go +++ b/client/clientset/versioned/fake/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/fake/register.go b/client/clientset/versioned/fake/register.go index 11a64ab..80fe2cb 100644 --- a/client/clientset/versioned/fake/register.go +++ b/client/clientset/versioned/fake/register.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/scheme/doc.go b/client/clientset/versioned/scheme/doc.go index 40e42c2..a3e95ed 100644 --- a/client/clientset/versioned/scheme/doc.go +++ b/client/clientset/versioned/scheme/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/scheme/register.go b/client/clientset/versioned/scheme/register.go index eb332b0..ffe93d2 100644 --- a/client/clientset/versioned/scheme/register.go +++ b/client/clientset/versioned/scheme/register.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go b/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go index 3c758ab..41e8750 100644 --- a/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go +++ b/client/clientset/versioned/typed/apis/v1alpha1/apis_client.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import ( type ApisV1alpha1Interface interface { RESTClient() rest.Interface ClusterProfilesGetter + PlacementDecisionsGetter } // ApisV1alpha1Client is used to interact with features provided by the apis group. @@ -39,6 +40,10 @@ func (c *ApisV1alpha1Client) ClusterProfiles(namespace string) ClusterProfileInt return newClusterProfiles(c, namespace) } +func (c *ApisV1alpha1Client) PlacementDecisions(namespace string) PlacementDecisionInterface { + return newPlacementDecisions(c, namespace) +} + // NewForConfig creates a new ApisV1alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/client/clientset/versioned/typed/apis/v1alpha1/clusterprofile.go b/client/clientset/versioned/typed/apis/v1alpha1/clusterprofile.go index 3ad86c7..c80f9f9 100644 --- a/client/clientset/versioned/typed/apis/v1alpha1/clusterprofile.go +++ b/client/clientset/versioned/typed/apis/v1alpha1/clusterprofile.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/typed/apis/v1alpha1/doc.go b/client/clientset/versioned/typed/apis/v1alpha1/doc.go index 28991e2..910e082 100644 --- a/client/clientset/versioned/typed/apis/v1alpha1/doc.go +++ b/client/clientset/versioned/typed/apis/v1alpha1/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go b/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go index fbfccbb..0183933 100644 --- a/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go +++ b/client/clientset/versioned/typed/apis/v1alpha1/fake/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go b/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go index 1fb9721..5d1ee1b 100644 --- a/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go +++ b/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_apis_client.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,6 +31,10 @@ func (c *FakeApisV1alpha1) ClusterProfiles(namespace string) v1alpha1.ClusterPro return newFakeClusterProfiles(c, namespace) } +func (c *FakeApisV1alpha1) PlacementDecisions(namespace string) v1alpha1.PlacementDecisionInterface { + return newFakePlacementDecisions(c, namespace) +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeApisV1alpha1) RESTClient() rest.Interface { diff --git a/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_clusterprofile.go b/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_clusterprofile.go index 2feb69e..c96aa4f 100644 --- a/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_clusterprofile.go +++ b/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_clusterprofile.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_placementdecision.go b/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_placementdecision.go new file mode 100644 index 0000000..f097091 --- /dev/null +++ b/client/clientset/versioned/typed/apis/v1alpha1/fake/fake_placementdecision.go @@ -0,0 +1,51 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + gentype "k8s.io/client-go/gentype" + v1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + apisv1alpha1 "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned/typed/apis/v1alpha1" +) + +// fakePlacementDecisions implements PlacementDecisionInterface +type fakePlacementDecisions struct { + *gentype.FakeClientWithList[*v1alpha1.PlacementDecision, *v1alpha1.PlacementDecisionList] + Fake *FakeApisV1alpha1 +} + +func newFakePlacementDecisions(fake *FakeApisV1alpha1, namespace string) apisv1alpha1.PlacementDecisionInterface { + return &fakePlacementDecisions{ + gentype.NewFakeClientWithList[*v1alpha1.PlacementDecision, *v1alpha1.PlacementDecisionList]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("placementdecisions"), + v1alpha1.SchemeGroupVersion.WithKind("PlacementDecision"), + func() *v1alpha1.PlacementDecision { return &v1alpha1.PlacementDecision{} }, + func() *v1alpha1.PlacementDecisionList { return &v1alpha1.PlacementDecisionList{} }, + func(dst, src *v1alpha1.PlacementDecisionList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.PlacementDecisionList) []*v1alpha1.PlacementDecision { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.PlacementDecisionList, items []*v1alpha1.PlacementDecision) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go b/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go index 72d2999..01aec23 100644 --- a/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go +++ b/client/clientset/versioned/typed/apis/v1alpha1/generated_expansion.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,3 +18,5 @@ limitations under the License. package v1alpha1 type ClusterProfileExpansion interface{} + +type PlacementDecisionExpansion interface{} diff --git a/client/clientset/versioned/typed/apis/v1alpha1/placementdecision.go b/client/clientset/versioned/typed/apis/v1alpha1/placementdecision.go new file mode 100644 index 0000000..54cb0d5 --- /dev/null +++ b/client/clientset/versioned/typed/apis/v1alpha1/placementdecision.go @@ -0,0 +1,67 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" + apisv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + scheme "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned/scheme" +) + +// PlacementDecisionsGetter has a method to return a PlacementDecisionInterface. +// A group's client should implement this interface. +type PlacementDecisionsGetter interface { + PlacementDecisions(namespace string) PlacementDecisionInterface +} + +// PlacementDecisionInterface has methods to work with PlacementDecision resources. +type PlacementDecisionInterface interface { + Create(ctx context.Context, placementDecision *apisv1alpha1.PlacementDecision, opts v1.CreateOptions) (*apisv1alpha1.PlacementDecision, error) + Update(ctx context.Context, placementDecision *apisv1alpha1.PlacementDecision, opts v1.UpdateOptions) (*apisv1alpha1.PlacementDecision, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*apisv1alpha1.PlacementDecision, error) + List(ctx context.Context, opts v1.ListOptions) (*apisv1alpha1.PlacementDecisionList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apisv1alpha1.PlacementDecision, err error) + PlacementDecisionExpansion +} + +// placementDecisions implements PlacementDecisionInterface +type placementDecisions struct { + *gentype.ClientWithList[*apisv1alpha1.PlacementDecision, *apisv1alpha1.PlacementDecisionList] +} + +// newPlacementDecisions returns a PlacementDecisions +func newPlacementDecisions(c *ApisV1alpha1Client, namespace string) *placementDecisions { + return &placementDecisions{ + gentype.NewClientWithList[*apisv1alpha1.PlacementDecision, *apisv1alpha1.PlacementDecisionList]( + "placementdecisions", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *apisv1alpha1.PlacementDecision { return &apisv1alpha1.PlacementDecision{} }, + func() *apisv1alpha1.PlacementDecisionList { return &apisv1alpha1.PlacementDecisionList{} }, + ), + } +} diff --git a/client/informers/externalversions/apis/interface.go b/client/informers/externalversions/apis/interface.go index bc9a56e..ddf7813 100644 --- a/client/informers/externalversions/apis/interface.go +++ b/client/informers/externalversions/apis/interface.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/informers/externalversions/apis/v1alpha1/clusterprofile.go b/client/informers/externalversions/apis/v1alpha1/clusterprofile.go index 94ca193..673a121 100644 --- a/client/informers/externalversions/apis/v1alpha1/clusterprofile.go +++ b/client/informers/externalversions/apis/v1alpha1/clusterprofile.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/informers/externalversions/apis/v1alpha1/interface.go b/client/informers/externalversions/apis/v1alpha1/interface.go index 2d79ec4..8c7d382 100644 --- a/client/informers/externalversions/apis/v1alpha1/interface.go +++ b/client/informers/externalversions/apis/v1alpha1/interface.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ import ( type Interface interface { // ClusterProfiles returns a ClusterProfileInformer. ClusterProfiles() ClusterProfileInformer + // PlacementDecisions returns a PlacementDecisionInformer. + PlacementDecisions() PlacementDecisionInformer } type version struct { @@ -42,3 +44,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList func (v *version) ClusterProfiles() ClusterProfileInformer { return &clusterProfileInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// PlacementDecisions returns a PlacementDecisionInformer. +func (v *version) PlacementDecisions() PlacementDecisionInformer { + return &placementDecisionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/client/informers/externalversions/apis/v1alpha1/placementdecision.go b/client/informers/externalversions/apis/v1alpha1/placementdecision.go new file mode 100644 index 0000000..b831985 --- /dev/null +++ b/client/informers/externalversions/apis/v1alpha1/placementdecision.go @@ -0,0 +1,89 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" + clusterinventoryapiapisv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + versioned "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned" + internalinterfaces "sigs.k8s.io/cluster-inventory-api/client/informers/externalversions/internalinterfaces" + apisv1alpha1 "sigs.k8s.io/cluster-inventory-api/client/listers/apis/v1alpha1" +) + +// PlacementDecisionInformer provides access to a shared informer and lister for +// PlacementDecisions. +type PlacementDecisionInformer interface { + Informer() cache.SharedIndexInformer + Lister() apisv1alpha1.PlacementDecisionLister +} + +type placementDecisionInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewPlacementDecisionInformer constructs a new informer for PlacementDecision type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPlacementDecisionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredPlacementDecisionInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredPlacementDecisionInformer constructs a new informer for PlacementDecision type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredPlacementDecisionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ApisV1alpha1().PlacementDecisions(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ApisV1alpha1().PlacementDecisions(namespace).Watch(context.TODO(), options) + }, + }, + &clusterinventoryapiapisv1alpha1.PlacementDecision{}, + resyncPeriod, + indexers, + ) +} + +func (f *placementDecisionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredPlacementDecisionInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *placementDecisionInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&clusterinventoryapiapisv1alpha1.PlacementDecision{}, f.defaultInformer) +} + +func (f *placementDecisionInformer) Lister() apisv1alpha1.PlacementDecisionLister { + return apisv1alpha1.NewPlacementDecisionLister(f.Informer().GetIndexer()) +} diff --git a/client/informers/externalversions/factory.go b/client/informers/externalversions/factory.go index 6e7d5fb..6c59018 100644 --- a/client/informers/externalversions/factory.go +++ b/client/informers/externalversions/factory.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/informers/externalversions/generic.go b/client/informers/externalversions/generic.go index 55c1bb3..86315af 100644 --- a/client/informers/externalversions/generic.go +++ b/client/informers/externalversions/generic.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -54,6 +54,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=apis, Version=v1alpha1 case v1alpha1.SchemeGroupVersion.WithResource("clusterprofiles"): return &genericInformer{resource: resource.GroupResource(), informer: f.Apis().V1alpha1().ClusterProfiles().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("placementdecisions"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Apis().V1alpha1().PlacementDecisions().Informer()}, nil } diff --git a/client/informers/externalversions/internalinterfaces/factory_interfaces.go b/client/informers/externalversions/internalinterfaces/factory_interfaces.go index 27fe82d..4385051 100644 --- a/client/informers/externalversions/internalinterfaces/factory_interfaces.go +++ b/client/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/listers/apis/v1alpha1/clusterprofile.go b/client/listers/apis/v1alpha1/clusterprofile.go index 7a722c9..06350f2 100644 --- a/client/listers/apis/v1alpha1/clusterprofile.go +++ b/client/listers/apis/v1alpha1/clusterprofile.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/client/listers/apis/v1alpha1/expansion_generated.go b/client/listers/apis/v1alpha1/expansion_generated.go index 3109bab..c37f08d 100644 --- a/client/listers/apis/v1alpha1/expansion_generated.go +++ b/client/listers/apis/v1alpha1/expansion_generated.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,3 +24,11 @@ type ClusterProfileListerExpansion interface{} // ClusterProfileNamespaceListerExpansion allows custom methods to be added to // ClusterProfileNamespaceLister. type ClusterProfileNamespaceListerExpansion interface{} + +// PlacementDecisionListerExpansion allows custom methods to be added to +// PlacementDecisionLister. +type PlacementDecisionListerExpansion interface{} + +// PlacementDecisionNamespaceListerExpansion allows custom methods to be added to +// PlacementDecisionNamespaceLister. +type PlacementDecisionNamespaceListerExpansion interface{} diff --git a/client/listers/apis/v1alpha1/placementdecision.go b/client/listers/apis/v1alpha1/placementdecision.go new file mode 100644 index 0000000..f247692 --- /dev/null +++ b/client/listers/apis/v1alpha1/placementdecision.go @@ -0,0 +1,69 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" + apisv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" +) + +// PlacementDecisionLister helps list PlacementDecisions. +// All objects returned here must be treated as read-only. +type PlacementDecisionLister interface { + // List lists all PlacementDecisions in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apisv1alpha1.PlacementDecision, err error) + // PlacementDecisions returns an object that can list and get PlacementDecisions. + PlacementDecisions(namespace string) PlacementDecisionNamespaceLister + PlacementDecisionListerExpansion +} + +// placementDecisionLister implements the PlacementDecisionLister interface. +type placementDecisionLister struct { + listers.ResourceIndexer[*apisv1alpha1.PlacementDecision] +} + +// NewPlacementDecisionLister returns a new PlacementDecisionLister. +func NewPlacementDecisionLister(indexer cache.Indexer) PlacementDecisionLister { + return &placementDecisionLister{listers.New[*apisv1alpha1.PlacementDecision](indexer, apisv1alpha1.Resource("placementdecision"))} +} + +// PlacementDecisions returns an object that can list and get PlacementDecisions. +func (s *placementDecisionLister) PlacementDecisions(namespace string) PlacementDecisionNamespaceLister { + return placementDecisionNamespaceLister{listers.NewNamespaced[*apisv1alpha1.PlacementDecision](s.ResourceIndexer, namespace)} +} + +// PlacementDecisionNamespaceLister helps list and get PlacementDecisions. +// All objects returned here must be treated as read-only. +type PlacementDecisionNamespaceLister interface { + // List lists all PlacementDecisions in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apisv1alpha1.PlacementDecision, err error) + // Get retrieves the PlacementDecision from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*apisv1alpha1.PlacementDecision, error) + PlacementDecisionNamespaceListerExpansion +} + +// placementDecisionNamespaceLister implements the PlacementDecisionNamespaceLister +// interface. +type placementDecisionNamespaceLister struct { + listers.ResourceIndexer[*apisv1alpha1.PlacementDecision] +} diff --git a/cmd/secretreader-plugin/main.go b/cmd/secretreader-plugin/main.go index 066cf99..5cf00c5 100644 --- a/cmd/secretreader-plugin/main.go +++ b/cmd/secretreader-plugin/main.go @@ -51,10 +51,14 @@ const SecretTokenKey = "token" func (Provider) Name() string { return ProviderName } -func (p Provider) GetToken(ctx context.Context, info clientauthenticationv1.ExecCredential) (clientauthenticationv1.ExecCredentialStatus, error) { +func (p Provider) GetToken( + ctx context.Context, + info clientauthenticationv1.ExecCredential, +) (clientauthenticationv1.ExecCredentialStatus, error) { // Require pre-initialized typed clients if p.KubeClient == nil { - return clientauthenticationv1.ExecCredentialStatus{}, errors.New("provider clients are not initialized; construct with NewDefault or set clients") + return clientauthenticationv1.ExecCredentialStatus{}, + errors.New("provider clients are not initialized; construct with NewDefault or set clients") } // Require clusterName to be present in extensions config @@ -67,21 +71,25 @@ func (p Provider) GetToken(ctx context.Context, info clientauthenticationv1.Exec } var cfg execClusterConfig if err := json.Unmarshal(info.Spec.Cluster.Config.Raw, &cfg); err != nil { - return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("invalid ExecCredential.Spec.Cluster.Config: %w", err) + return clientauthenticationv1.ExecCredentialStatus{}, + fmt.Errorf("invalid ExecCredential.Spec.Cluster.Config: %w", err) } if cfg.ClusterName == "" { - return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("missing clusterName in ExecCredential.Spec.Cluster.Config") + return clientauthenticationv1.ExecCredentialStatus{}, + fmt.Errorf("missing clusterName in ExecCredential.Spec.Cluster.Config") } clusterName := cfg.ClusterName // Read Secret / via typed client and return token sec, err := p.KubeClient.CoreV1().Secrets(p.Namespace).Get(ctx, clusterName, metav1.GetOptions{}) if err != nil { - return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("failed to get secret %s/%s: %w", p.Namespace, clusterName, err) + return clientauthenticationv1.ExecCredentialStatus{}, + fmt.Errorf("failed to get secret %s/%s: %w", p.Namespace, clusterName, err) } data, ok := sec.Data[SecretTokenKey] if !ok || len(data) == 0 { - return clientauthenticationv1.ExecCredentialStatus{}, fmt.Errorf("secret %s/%s missing %q key", p.Namespace, clusterName, SecretTokenKey) + return clientauthenticationv1.ExecCredentialStatus{}, + fmt.Errorf("secret %s/%s missing %q key", p.Namespace, clusterName, SecretTokenKey) } return clientauthenticationv1.ExecCredentialStatus{Token: string(data)}, nil diff --git a/config/crd/bases/_.yaml b/config/crd/bases/_.yaml deleted file mode 100644 index 087f8a2..0000000 --- a/config/crd/bases/_.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.17.3 -spec: - group: "" - names: - kind: "" - plural: "" - scope: "" - versions: null diff --git a/config/crd/bases/multicluster.x-k8s.io_placementdecisions.yaml b/config/crd/bases/multicluster.x-k8s.io_placementdecisions.yaml new file mode 100644 index 0000000..b7a3c34 --- /dev/null +++ b/config/crd/bases/multicluster.x-k8s.io_placementdecisions.yaml @@ -0,0 +1,120 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: placementdecisions.multicluster.x-k8s.io +spec: + group: multicluster.x-k8s.io + names: + kind: PlacementDecision + listKind: PlacementDecisionList + plural: placementdecisions + singular: placementdecision + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PlacementDecision publishes the set of clusters chosen by a scheduler at a point in time. + It is a data-only resource that acts as the interface between schedulers and consumers. + Schedulers write decisions using this format; consumers read from it. + + Following the EndpointSlice convention, a single scheduling decision can fan out to N + PlacementDecision slices, each limited to 100 clusters. To correlate slices, producers + MUST set the same multicluster.x-k8s.io/decision-key label on all slices when more than + one slice exists. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + decisions: + description: |- + Decisions is the list of clusters chosen for this placement decision. + Up to 100 ClusterDecisions per object (slice) to stay well below the etcd limit. + items: + description: ClusterDecision references a target ClusterProfile to apply + workloads to. + properties: + clusterProfileRef: + description: |- + ClusterProfileRef is a reference to the target ClusterProfile. + The reference must point to a valid ClusterProfile in the fleet. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + reason: + description: |- + Reason is an optional explanation of why this cluster was chosen. + This can be useful for debugging and auditing placement decisions. + type: string + required: + - clusterProfileRef + type: object + maxItems: 100 + minItems: 0 + type: array + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + schedulerName: + description: |- + SchedulerName is the name of the scheduler that created this decision. + This is optional and can be used for debugging and auditing purposes. + type: string + required: + - decisions + type: object + served: true + storage: true diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 4ad4385..8057371 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,5 +1,5 @@ /* -Copyright 2024 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/hack/kube-env.sh b/hack/kube-env.sh new file mode 100755 index 0000000..a20d17f --- /dev/null +++ b/hack/kube-env.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Copyright 2025 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Some useful colors. +if [[ -z "${color_start-}" ]]; then + declare -r color_start="\033[" + declare -r color_red="${color_start}0;31m" + declare -r color_yellow="${color_start}0;33m" + declare -r color_green="${color_start}0;32m" + declare -r color_norm="${color_start}0m" +fi diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 089419e..e7b075a 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2024 The Kubernetes Authors. +# Copyright 2025 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,25 +18,47 @@ set -o errexit set -o nounset set -o pipefail -set -o errexit -set -o nounset -set -o pipefail - SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. -CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)} -source "${CODEGEN_PKG}/kube_codegen.sh" +cd "${SCRIPT_ROOT}" + +# Install code-generator tools +echo "Installing code-generator tools..." +go install k8s.io/code-generator/cmd/client-gen@v0.32.1 +go install k8s.io/code-generator/cmd/lister-gen@v0.32.1 +go install k8s.io/code-generator/cmd/informer-gen@v0.32.1 + +# Go installs the above commands to $GOBIN if defined, and $GOPATH/bin otherwise +GOBIN="$(go env GOBIN)" +gobin="${GOBIN:-$(go env GOPATH)/bin}" THIS_PKG="sigs.k8s.io/cluster-inventory-api" +OUTPUT_PKG="${THIS_PKG}/client" +FQ_APIS="${THIS_PKG}/apis/v1alpha1" + +echo "Generating clientset at ${OUTPUT_PKG}/clientset" +"${gobin}/client-gen" \ + --go-header-file "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ + --input-base="" \ + --input="${FQ_APIS}" \ + --output-pkg="${OUTPUT_PKG}/clientset" \ + --output-dir="client/clientset" \ + --clientset-name=versioned + +echo "Generating listers at ${OUTPUT_PKG}/listers" +"${gobin}/lister-gen" \ + --go-header-file "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ + --output-pkg="${OUTPUT_PKG}/listers" \ + --output-dir="client/listers" \ + "${FQ_APIS}" + +echo "Generating informers at ${OUTPUT_PKG}/informers" +"${gobin}/informer-gen" \ + --go-header-file "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ + --versioned-clientset-package="${OUTPUT_PKG}/clientset/versioned" \ + --listers-package="${OUTPUT_PKG}/listers" \ + --output-pkg="${OUTPUT_PKG}/informers" \ + --output-dir="client/informers" \ + "${FQ_APIS}" -kube::codegen::gen_helpers \ - --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ - "${SCRIPT_ROOT}" - -kube::codegen::gen_client \ - --with-watch \ - --output-dir "${SCRIPT_ROOT}/client" \ - --output-pkg "${THIS_PKG}/client" \ - --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \ - --one-input-api apis \ - "${SCRIPT_ROOT}" +echo "Code generation completed." diff --git a/hack/verify-all.sh b/hack/verify-all.sh new file mode 100755 index 0000000..dcf8be9 --- /dev/null +++ b/hack/verify-all.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Copyright 2025 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. +source "${SCRIPT_ROOT}/hack/kube-env.sh" + +SILENT=true + +function is-excluded { + for e in $EXCLUDE; do + if [[ $1 -ef ${BASH_SOURCE} ]]; then + return + fi + if [[ $1 -ef "$SCRIPT_ROOT/hack/$e" ]]; then + return + fi + done + return 1 +} + +while getopts ":v" opt; do + case $opt in + v) + SILENT=false + ;; + \?) + echo "Invalid flag: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +if $SILENT ; then + echo "Running in the silent mode, run with -v if you want to see script logs." +fi + +EXCLUDE="verify-all.sh" + +ret=0 +for t in $(ls $SCRIPT_ROOT/hack/verify-*.sh) +do + if is-excluded $t ; then + echo "Skipping $t" + continue + fi + if $SILENT ; then + echo -e "Verifying $t" + if bash "$t" &> /dev/null; then + echo -e "${color_green}SUCCESS${color_norm}" + else + echo -e "${color_red}FAILED${color_norm}" + ret=1 + fi + else + bash "$t" || ret=1 + fi +done + +exit $ret diff --git a/hack/verify-codegen.sh b/hack/verify-codegen.sh new file mode 100755 index 0000000..429f01a --- /dev/null +++ b/hack/verify-codegen.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Copyright 2025 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. + +cd $SCRIPT_ROOT + +# Create temp directories for diffing +DIFFROOT_APIS="${SCRIPT_ROOT}/apis" +DIFFROOT_CLIENT="${SCRIPT_ROOT}/client" +TMP_DIFFROOT_APIS="${SCRIPT_ROOT}/_tmp/apis" +TMP_DIFFROOT_CLIENT="${SCRIPT_ROOT}/_tmp/client" +_tmp="${SCRIPT_ROOT}/_tmp" + +cleanup() { + rm -rf "${_tmp}" +} +trap "cleanup" EXIT SIGINT + +cleanup + +mkdir -p "${TMP_DIFFROOT_APIS}" +mkdir -p "${TMP_DIFFROOT_CLIENT}" +cp -a "${DIFFROOT_APIS}"/* "${TMP_DIFFROOT_APIS}" +cp -a "${DIFFROOT_CLIENT}"/* "${TMP_DIFFROOT_CLIENT}" + +bash "${SCRIPT_ROOT}/hack/update-codegen.sh" + +echo "diffing ${DIFFROOT_APIS} against freshly generated codegen" +ret=0 +diff -Naupr "${DIFFROOT_APIS}" "${TMP_DIFFROOT_APIS}" || ret=$? +if [[ $ret -ne 0 ]]; then + cp -a "${TMP_DIFFROOT_APIS}"/* "${DIFFROOT_APIS}" + echo "${DIFFROOT_APIS} is out of date. Please run hack/update-codegen.sh" + exit 1 +fi +echo "${DIFFROOT_APIS} up to date." + +echo "diffing ${DIFFROOT_CLIENT} against freshly generated codegen" +diff -Naupr "${DIFFROOT_CLIENT}" "${TMP_DIFFROOT_CLIENT}" || ret=$? +if [[ $ret -ne 0 ]]; then + cp -a "${TMP_DIFFROOT_CLIENT}"/* "${DIFFROOT_CLIENT}" + echo "${DIFFROOT_CLIENT} is out of date. Please run hack/update-codegen.sh" + exit 1 +fi +echo "${DIFFROOT_CLIENT} up to date." + +cp -a "${TMP_DIFFROOT_APIS}"/* "${DIFFROOT_APIS}" +cp -a "${TMP_DIFFROOT_CLIENT}"/* "${DIFFROOT_CLIENT}" diff --git a/hack/verify-crds.sh b/hack/verify-crds.sh new file mode 100755 index 0000000..f884264 --- /dev/null +++ b/hack/verify-crds.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Copyright 2025 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. +DIFFROOT="${SCRIPT_ROOT}/config/crd" +TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/config/crd" +_tmp="${SCRIPT_ROOT}/_tmp" + +cd "${SCRIPT_ROOT}" + +cleanup() { + rm -rf "${_tmp}" +} +trap "cleanup" EXIT SIGINT + +cleanup + +mkdir -p "${TMP_DIFFROOT}" +cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}" + +# Run make manifests to generate CRDs +make manifests + +echo "diffing ${DIFFROOT} against freshly generated CRDs" +ret=0 +diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? +if [[ $ret -eq 0 ]] +then + echo "${DIFFROOT} up to date." +else + cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" + echo "${DIFFROOT} is out of date. Please run 'make manifests'" + exit 1 +fi diff --git a/hack/verify-gofmt.sh b/hack/verify-gofmt.sh new file mode 100755 index 0000000..ec24103 --- /dev/null +++ b/hack/verify-gofmt.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Copyright 2025 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_ROOT=$(dirname "${BASH_SOURCE}")/.. + +cd "${SCRIPT_ROOT}" + +find_files() { + find . -not \( \ + \( \ + -wholename './.git' \ + -o -wholename '*/vendor/*' \ + \) -prune \ + \) -name '*.go' +} + +GOFMT="gofmt -s" +bad_files=$(find_files | xargs $GOFMT -l) +if [[ -n "${bad_files}" ]]; then + echo "!!! '$GOFMT' needs to be run on the following files: " + echo "${bad_files}" + exit 1 +fi diff --git a/pkg/credentialplugin/core.go b/pkg/credentialplugin/core.go index bbb6516..3c1ad13 100644 --- a/pkg/credentialplugin/core.go +++ b/pkg/credentialplugin/core.go @@ -39,7 +39,10 @@ func readExecInfo() (*clientauthenticationv1.ExecCredential, error) { // Provider defines the common interface for all credential plugins type Provider interface { Name() string - GetToken(ctx context.Context, in clientauthenticationv1.ExecCredential) (clientauthenticationv1.ExecCredentialStatus, error) + GetToken( + ctx context.Context, + in clientauthenticationv1.ExecCredential, + ) (clientauthenticationv1.ExecCredentialStatus, error) } // Run is the common entrypoint used by all provider-specific binaries diff --git a/pkg/credentials/config.go b/pkg/credentials/config.go index 84b2623..72dc8ee 100644 --- a/pkg/credentials/config.go +++ b/pkg/credentials/config.go @@ -17,8 +17,7 @@ import ( // client.authentication.k8s.io/exec is a reserved extension key defined by the Kubernetes // client authentication API (SIG Auth), not by the ClusterProfile API. -// Reference: -// https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/#client-authentication-k8s-io-v1beta1-Cluster +// Reference: https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/ const clusterExtensionKey = "client.authentication.k8s.io/exec" type Provider struct { @@ -36,10 +35,15 @@ func New(providers []Provider) *CredentialsProvider { } } -// SetupProviderFileFlag defines the -clusterprofile-provider-file command-line flag and returns a pointer -// to the string that will hold the path. flag.Parse() must still be called manually by the caller +// SetupProviderFileFlag defines the -clusterprofile-provider-file command-line flag +// and returns a pointer to the string that will hold the path. +// flag.Parse() must still be called manually by the caller func SetupProviderFileFlag() *string { - return flag.String("clusterprofile-provider-file", "clusterprofile-provider-file.json", "Path to the JSON configuration file") + return flag.String( + "clusterprofile-provider-file", + "clusterprofile-provider-file.json", + "Path to the JSON configuration file", + ) } func NewFromFile(path string) (*CredentialsProvider, error) { @@ -117,13 +121,18 @@ func (cp *CredentialsProvider) getExecConfigFromConfig(providerName string) *cli // getClusterAccessorFromClusterProfile returns the first AccessProvider from the ClusterProfile // that matches one of the supported provider types in the CredentialsProvider -func (cp *CredentialsProvider) getClusterAccessorFromClusterProfile(cluster *v1alpha1.ClusterProfile) *v1alpha1.AccessProvider { +func (cp *CredentialsProvider) getClusterAccessorFromClusterProfile( + cluster *v1alpha1.ClusterProfile, +) *v1alpha1.AccessProvider { accessProviderTypes := map[string]*v1alpha1.AccessProvider{} // to keep backward compatibility, we first check the CredentialProviders field for _, accessProvider := range cluster.Status.CredentialProviders { accessProviderTypes[accessProvider.Name] = accessProvider.DeepCopy() - klog.Warningf("ClusterProfile %q uses deprecated field CredentialProviders %q; please migrate to AccessProviders", cluster.Name, accessProvider.Name) + klog.Warningf( + "ClusterProfile %q uses deprecated field CredentialProviders %q; please migrate to AccessProviders", + cluster.Name, accessProvider.Name, + ) } for _, accessProvider := range cluster.Status.AccessProviders { diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go index dbe8095..0aef20f 100644 --- a/test/integration/suite_test.go +++ b/test/integration/suite_test.go @@ -18,12 +18,13 @@ package integration import ( "context" + "path/filepath" + "testing" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" "k8s.io/client-go/rest" - "path/filepath" - "testing" "github.com/onsi/ginkgo" "github.com/onsi/gomega"