From fd67ba0cfe2678b0ff240dcf3ad2c6d04023ad5f Mon Sep 17 00:00:00 2001 From: Yaroslava Serdiuk Date: Fri, 17 Nov 2023 12:13:53 +0000 Subject: [PATCH] Implement ProvReq service --- .../provreqwrapper/wrapper.go | 137 ++++++++++++++++ .../provreqwrapper/wrapper_test.go | 142 ++++++++++++++++ .../provisioningrequest/service/service.go | 72 ++++++++ .../service/v1beta1client/client.go | 145 +++++++++++++++++ .../service/v1beta1client/client_test.go | 44 +++++ .../service/v1beta1client/testutils.go | 154 ++++++++++++++++++ 6 files changed, 694 insertions(+) create mode 100644 cluster-autoscaler/provisioningrequest/provreqwrapper/wrapper.go create mode 100644 cluster-autoscaler/provisioningrequest/provreqwrapper/wrapper_test.go create mode 100644 cluster-autoscaler/provisioningrequest/service/service.go create mode 100644 cluster-autoscaler/provisioningrequest/service/v1beta1client/client.go create mode 100644 cluster-autoscaler/provisioningrequest/service/v1beta1client/client_test.go create mode 100644 cluster-autoscaler/provisioningrequest/service/v1beta1client/testutils.go diff --git a/cluster-autoscaler/provisioningrequest/provreqwrapper/wrapper.go b/cluster-autoscaler/provisioningrequest/provreqwrapper/wrapper.go new file mode 100644 index 000000000000..c6cdb364108a --- /dev/null +++ b/cluster-autoscaler/provisioningrequest/provreqwrapper/wrapper.go @@ -0,0 +1,137 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provreqwrapper + +import ( + "fmt" + "strings" + + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/apis/autoscaling.x-k8s.io/v1beta1" +) + +// ProvisioningRequest wrapper representation of the ProvisisoningRequest +type ProvisioningRequest struct { + v1Beta1PR *v1beta1.ProvisioningRequest + v1Beta1PodTemplates []*apiv1.PodTemplate +} + +// PodSet wrapper representation of the PodSet. +type PodSet struct { + // Count number of pods with given template. + Count int32 + // PodTemplate template of given pod set. + PodTemplate apiv1.PodTemplateSpec +} + +// NewV1Beta1ProvisioningRequest creates new ProvisioningRequest based on v1beta1 CR. +func NewV1Beta1ProvisioningRequest(v1Beta1PR *v1beta1.ProvisioningRequest, v1Beta1PodTemplates []*apiv1.PodTemplate) *ProvisioningRequest { + return &ProvisioningRequest{ + v1Beta1PR: v1Beta1PR, + v1Beta1PodTemplates: v1Beta1PodTemplates, + } +} + +// Name of the Provisioning Request. +func (pr *ProvisioningRequest) Name() string { + return pr.v1Beta1PR.Name +} + +// Namespace of the Provisioning Request. +func (pr *ProvisioningRequest) Namespace() string { + return pr.v1Beta1PR.Namespace +} + +// CreationTimestamp of the Provisioning Request. +func (pr *ProvisioningRequest) CreationTimestamp() metav1.Time { + return pr.v1Beta1PR.CreationTimestamp +} + +// RuntimeObject returns runtime.Object of the Provisioning Request. +func (pr *ProvisioningRequest) RuntimeObject() runtime.Object { + return pr.v1Beta1PR +} + +// APIVersion returns APIVersion of the Provisioning Request. +func (pr *ProvisioningRequest) APIVersion() string { + return pr.v1Beta1PR.APIVersion +} + +// Kind returns Kind of the Provisioning Request. +func (pr *ProvisioningRequest) Kind() string { + return pr.v1Beta1PR.Kind + +} + +// UID returns UID of the Provisioning Request. +func (pr *ProvisioningRequest) UID() types.UID { + return pr.v1Beta1PR.UID +} + +// Conditions of the Provisioning Request. +func (pr *ProvisioningRequest) Conditions() []metav1.Condition { + return pr.v1Beta1PR.Status.Conditions +} + +// SetConditions of the Provisioning Request. +func (pr *ProvisioningRequest) SetConditions(conditions []metav1.Condition) { + pr.v1Beta1PR.Status.Conditions = conditions + return +} + +// PodSets of the Provisioning Request. +func (pr *ProvisioningRequest) PodSets() ([]PodSet, error) { + if len(pr.v1Beta1PR.Spec.PodSets) != len(pr.v1Beta1PodTemplates) { + return nil, errMissingPodTemplates(pr.v1Beta1PR.Spec.PodSets, pr.v1Beta1PodTemplates) + } + podSets := make([]PodSet, 0, len(pr.v1Beta1PR.Spec.PodSets)) + for i, podSet := range pr.v1Beta1PR.Spec.PodSets { + podSets = append(podSets, PodSet{ + Count: podSet.Count, + PodTemplate: pr.v1Beta1PodTemplates[i].Template, + }) + } + return podSets, nil +} + +// V1Beta1 returns v1beta1 object CR, to be used only to pass information to clients. +func (pr *ProvisioningRequest) V1Beta1() *v1beta1.ProvisioningRequest { + return pr.v1Beta1PR +} + +// PodTemplates returns pod templates associated with the Provisioning Request, to be used only to pass information to clients. +func (pr *ProvisioningRequest) PodTemplates() []*apiv1.PodTemplate { + return pr.v1Beta1PodTemplates +} + +// errMissingPodTemplates creates error that is passed when there are missing pod templates. +func errMissingPodTemplates(podSets []v1beta1.PodSet, podTemplates []*apiv1.PodTemplate) error { + foundPodTemplates := map[string]struct{}{} + for _, pt := range podTemplates { + foundPodTemplates[pt.Name] = struct{}{} + } + missingTemplates := make([]string, 0) + for _, ps := range podSets { + if _, found := foundPodTemplates[ps.PodTemplateRef.Name]; !found { + missingTemplates = append(missingTemplates, ps.PodTemplateRef.Name) + } + } + return fmt.Errorf("missing pod templates, %d pod templates were referenced, %d templates were missing: %s", len(podSets), len(missingTemplates), strings.Join(missingTemplates, ",")) +} diff --git a/cluster-autoscaler/provisioningrequest/provreqwrapper/wrapper_test.go b/cluster-autoscaler/provisioningrequest/provreqwrapper/wrapper_test.go new file mode 100644 index 000000000000..8b5359040df2 --- /dev/null +++ b/cluster-autoscaler/provisioningrequest/provreqwrapper/wrapper_test.go @@ -0,0 +1,142 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provreqwrapper + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/apis/autoscaling.x-k8s.io/v1beta1" +) + +func TestProvisioningRequestWrapper(t *testing.T) { + creationTimestamp := metav1.NewTime(time.Date(2023, 11, 12, 13, 14, 15, 0, time.UTC)) + conditions := []metav1.Condition{ + { + LastTransitionTime: metav1.NewTime(time.Date(2022, 11, 12, 13, 14, 15, 0, time.UTC)), + Message: "Message", + ObservedGeneration: 1, + Reason: "Reason", + Status: "Status", + Type: "ConditionType", + }, + } + podSets := []PodSet{ + { + Count: 1, + PodTemplate: apiv1.PodTemplateSpec{ + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "test-container", + Image: "test-image", + }, + }, + }, + }, + }, + } + + podTemplates := []*apiv1.PodTemplate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "name-pod-template-beta", + Namespace: "namespace-beta", + CreationTimestamp: creationTimestamp, + }, + Template: apiv1.PodTemplateSpec{ + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "test-container", + Image: "test-image", + }, + }, + }, + }, + }, + } + v1Beta1PR := &v1beta1.ProvisioningRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "beta-api", + Kind: "beta-kind", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "name-beta", + Namespace: "namespace-beta", + CreationTimestamp: creationTimestamp, + UID: types.UID("beta-uid"), + }, + Spec: v1beta1.ProvisioningRequestSpec{ + ProvisioningClassName: "queued-provisioning.gke.io", + PodSets: []v1beta1.PodSet{ + { + Count: 1, + PodTemplateRef: v1beta1.Reference{ + Name: podTemplates[0].Name, + }, + }, + }, + }, + Status: v1beta1.ProvisioningRequestStatus{ + Conditions: conditions, + ProvisioningClassDetails: map[string]v1beta1.Detail{}, + }, + } + + wrappedBetaPR := NewV1Beta1ProvisioningRequest(v1Beta1PR, podTemplates) + + // Check Name, Namespace and Creation accessors + assert.Equal(t, "name-beta", wrappedBetaPR.Name()) + assert.Equal(t, "namespace-beta", wrappedBetaPR.Namespace()) + assert.Equal(t, creationTimestamp, wrappedBetaPR.CreationTimestamp()) + + // Check APIVersion, Kind and UID accessors + assert.Equal(t, "beta-api", wrappedBetaPR.APIVersion()) + assert.Equal(t, "beta-kind", wrappedBetaPR.Kind()) + assert.Equal(t, types.UID("beta-uid"), wrappedBetaPR.UID()) + + // Check the initial conditions + assert.Equal(t, conditions, wrappedBetaPR.Conditions()) + + // Clear conditions and check the values + wrappedBetaPR.SetConditions(nil) + assert.Nil(t, wrappedBetaPR.Conditions()) + + // Set conditions and check the values + wrappedBetaPR.SetConditions(conditions) + assert.Equal(t, conditions, wrappedBetaPR.Conditions()) + + // Check the PodSets + betaPodSets, betaErr := wrappedBetaPR.PodSets() + assert.Nil(t, betaErr) + assert.Equal(t, podSets, betaPodSets) + + // Check the type accessors. + assert.Equal(t, v1Beta1PR, wrappedBetaPR.V1Beta1()) + assert.Equal(t, podTemplates, wrappedBetaPR.PodTemplates()) + + // Check case where the Provisioning Request is missing Pod Templates. + wrappedBetaPRMissingPodTemplates := NewV1Beta1ProvisioningRequest(v1Beta1PR, nil) + podSets, err := wrappedBetaPRMissingPodTemplates.PodSets() + assert.Nil(t, podSets) + assert.EqualError(t, err, "missing pod templates, 1 pod templates were referenced, 1 templates were missing: name-pod-template-beta") +} diff --git a/cluster-autoscaler/provisioningrequest/service/service.go b/cluster-autoscaler/provisioningrequest/service/service.go new file mode 100644 index 000000000000..72ffe9e1a43f --- /dev/null +++ b/cluster-autoscaler/provisioningrequest/service/service.go @@ -0,0 +1,72 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provreqservice + +import ( + "fmt" + + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/provreqwrapper" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/service/v1beta1client" + "k8s.io/client-go/rest" +) + +// ProvisioningRequestService represents the service that is able to list, +// access and delete different Provisioning Requests. +type ProvisioningRequestService struct { + provReqV1Beta1Client *v1beta1client.ProvisioningRequestClient +} + +// NewProvisioningRequestService returns new service for interacting with ProvisioningRequests. +func NewProvisioningRequestService(kubeConfig *rest.Config) (*ProvisioningRequestService, error) { + v1Beta1Client, err := v1beta1client.NewProvisioningRequestClient(kubeConfig) + if err != nil { + return nil, err + } + return &ProvisioningRequestService{ + provReqV1Beta1Client: v1Beta1Client, + }, nil +} + +// ProvisioningRequest gets a specific ProvisioningRequest CR. +func (s *ProvisioningRequestService) ProvisioningRequest(namespace, name string) (*provreqwrapper.ProvisioningRequest, error) { + v1Beta1PR, err := s.provReqV1Beta1Client.ProvisioningRequest(namespace, name) + if err == nil { + podTemplates, errPodTemplates := s.provReqV1Beta1Client.FetchPodTemplates(v1Beta1PR) + if errPodTemplates != nil { + return nil, fmt.Errorf("while fetching pod templates for Get Provisioning Request %s/%s got error: %v", namespace, name, errPodTemplates) + } + return provreqwrapper.NewV1Beta1ProvisioningRequest(v1Beta1PR, podTemplates), nil + } + return nil, err +} + +// ProvisioningRequests gets all Queued ProvisioningRequest CRs. +func (s *ProvisioningRequestService) ProvisioningRequests() ([]*provreqwrapper.ProvisioningRequest, error) { + v1Beta1PRs, err := s.provReqV1Beta1Client.ProvisioningRequests() + if err != nil { + return nil, err + } + prs := make([]*provreqwrapper.ProvisioningRequest, 0, len(v1Beta1PRs)) + for _, v1Beta1PR := range v1Beta1PRs { + podTemplates, errPodTemplates := s.provReqV1Beta1Client.FetchPodTemplates(v1Beta1PR) + if errPodTemplates != nil { + return nil, fmt.Errorf("while fetching pod templates for List Provisioning Request %s/%s got error: %v", v1Beta1PR.Namespace, v1Beta1PR.Name, errPodTemplates) + } + prs = append(prs, provreqwrapper.NewV1Beta1ProvisioningRequest(v1Beta1PR, podTemplates)) + } + return prs, nil +} diff --git a/cluster-autoscaler/provisioningrequest/service/v1beta1client/client.go b/cluster-autoscaler/provisioningrequest/service/v1beta1client/client.go new file mode 100644 index 000000000000..f181ef8c807a --- /dev/null +++ b/cluster-autoscaler/provisioningrequest/service/v1beta1client/client.go @@ -0,0 +1,145 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1client + +import ( + "fmt" + "time" + + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/apis/autoscaling.x-k8s.io/v1beta1" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/client/clientset/versioned" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/client/informers/externalversions" + listers "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/client/listers/autoscaling.x-k8s.io/v1beta1" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + v1 "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/rest" + + klog "k8s.io/klog/v2" +) + +const ( + provisioningRequestClientCallTimeout = 4 * time.Second +) + +// ProvisioningRequestClient represents client for v1beta1 ProvReq CRD. +type ProvisioningRequestClient struct { + client versioned.Interface + prLister listers.ProvisioningRequestLister + ptLister v1.PodTemplateLister +} + +// NewProvisioningRequestClient configures and returns a provisioningRequestClient. +func NewProvisioningRequestClient(kubeConfig *rest.Config) (*ProvisioningRequestClient, error) { + prClient, err := newPRClient(kubeConfig) + if err != nil { + klog.Warningf("Failed to create Provisioning Request client, will not use Provisioning Requests. Error was: %v", err) + return nil, err + } + + prLister, err := newAllPRsLister(prClient, make(chan struct{})) + if err != nil { + klog.Warningf("Failed to create Provisioning Request lister, will not use Provisioning Requests. Error was: %v", err) + return nil, err + } + + ptLister, err := newPodTemplatesLister(kubeConfig, make(chan struct{})) + if err != nil { + klog.Warningf("Failed to create Pod Template lister, will not use Provisioning Requests. Error was: %v", err) + return nil, err + } + + return &ProvisioningRequestClient{ + client: prClient, + prLister: prLister, + ptLister: ptLister, + }, nil +} + +// ProvisioningRequests gets a specific ProvisioningRequest CR. +func (c *ProvisioningRequestClient) ProvisioningRequest(namespace, name string) (*v1beta1.ProvisioningRequest, error) { + return c.prLister.ProvisioningRequests(namespace).Get(name) +} + +// ProvisioningRequests gets all ProvisioningRequest CRs. +func (c *ProvisioningRequestClient) ProvisioningRequests() ([]*v1beta1.ProvisioningRequest, error) { + provisioningRequests, err := c.prLister.List(labels.Everything()) + if err != nil { + return nil, fmt.Errorf("error fetching provisioningRequests: %w", err) + } + return provisioningRequests, nil +} + +// FetchPodTemplates fetches PodTemplates referenced by the Provisioning Request. +func (c *ProvisioningRequestClient) FetchPodTemplates(pr *v1beta1.ProvisioningRequest) ([]*apiv1.PodTemplate, error) { + podTemplates := make([]*apiv1.PodTemplate, 0, len(pr.Spec.PodSets)) + for _, podSpec := range pr.Spec.PodSets { + podTemplate, err := c.ptLister.PodTemplates(pr.Namespace).Get(podSpec.PodTemplateRef.Name) + if errors.IsNotFound(err) { + klog.Infof("While fetching Pod Template for Provisioning Request %s/%s received not found error", pr.Namespace, pr.Name) + continue + } else if err != nil { + return nil, err + } + podTemplates = append(podTemplates, podTemplate) + } + return podTemplates, nil +} + +// newPRClient creates a new Provisioning Request client from the given config. +func newPRClient(kubeConfig *rest.Config) (*versioned.Clientset, error) { + return versioned.NewForConfig(kubeConfig) +} + +// newAllPRsLister creates a lister for the Provisioning Requests in the cluster. +func newAllPRsLister(prClient versioned.Interface, stopChannel <-chan struct{}) (listers.ProvisioningRequestLister, error) { + factory := externalversions.NewSharedInformerFactory(prClient, 1*time.Hour) + prLister := factory.Autoscaling().V1beta1().ProvisioningRequests().Lister() + factory.Start(stopChannel) + informersSynced := factory.WaitForCacheSync(stopChannel) + for _, synced := range informersSynced { + if !synced { + return nil, fmt.Errorf("can't create Provisioning Request lister") + } + } + klog.V(2).Info("Successful initial Provisioning Request sync") + return prLister, nil +} + +// newPodTemplatesLister creates a lister for the Pod Templates in the cluster. +func newPodTemplatesLister(kubeConfig *rest.Config, stopChannel <-chan struct{}) (v1.PodTemplateLister, error) { + client, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + klog.Warningf("Failed to create Pod Template lister, will not use Provisioning Requests Error was: %v", err) + return nil, err + } + factory := informers.NewSharedInformerFactory(client, 1*time.Hour) + ptLister := factory.Core().V1().PodTemplates().Lister() + factory.Start(stopChannel) + informersSynced := factory.WaitForCacheSync(stopChannel) + for _, synced := range informersSynced { + if !synced { + return nil, fmt.Errorf("can't create Pod Template lister") + } + } + klog.V(2).Info("Successful initial Pod Template sync") + return ptLister, nil +} diff --git a/cluster-autoscaler/provisioningrequest/service/v1beta1client/client_test.go b/cluster-autoscaler/provisioningrequest/service/v1beta1client/client_test.go new file mode 100644 index 000000000000..9ba061ef826a --- /dev/null +++ b/cluster-autoscaler/provisioningrequest/service/v1beta1client/client_test.go @@ -0,0 +1,44 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1client + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/provreqwrapper" +) + +func TestFetchPodTemplates(t *testing.T) { + pr1 := ProvisioningRequestBetaForTests("namespace", "name-1") + pr2 := ProvisioningRequestBetaForTests("namespace", "name-2") + mockProvisioningRequests := []*provreqwrapper.ProvisioningRequest{pr1, pr2} + + ctx := context.Background() + c, _ := NewFakeProvisioningRequestClient(t, ctx, mockProvisioningRequests...) + got, err := c.FetchPodTemplates(pr1.V1Beta1()) + if err != nil { + t.Errorf("provisioningRequestClient.ProvisioningRequests() error: %v", err) + } + if len(got) != 1 { + t.Errorf("provisioningRequestClient.ProvisioningRequests() got: %v, want 1 element", err) + } + if diff := cmp.Diff(pr1.PodTemplates(), got); diff != "" { + t.Errorf("Template mismatch, diff (-want +got):\n%s", diff) + } +} diff --git a/cluster-autoscaler/provisioningrequest/service/v1beta1client/testutils.go b/cluster-autoscaler/provisioningrequest/service/v1beta1client/testutils.go new file mode 100644 index 000000000000..9141372acd81 --- /dev/null +++ b/cluster-autoscaler/provisioningrequest/service/v1beta1client/testutils.go @@ -0,0 +1,154 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1client + +import ( + "context" + "fmt" + "testing" + "time" + + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/apis/autoscaling.x-k8s.io/v1beta1" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/client/clientset/versioned/fake" + "k8s.io/autoscaler/cluster-autoscaler/provisioningrequest/provreqwrapper" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + fake_kubernetes "k8s.io/client-go/kubernetes/fake" + v1 "k8s.io/client-go/listers/core/v1" + klog "k8s.io/klog/v2" +) + +type FakeBetaProvisiningRequest struct { + ProvisioningRequest *v1beta1.ProvisioningRequest + PodTemplates []*apiv1.PodTemplate +} + +func NewFakeProvisioningRequestClient(t *testing.T, ctx context.Context, prs ...*provreqwrapper.ProvisioningRequest) (*ProvisioningRequestClient, *FakeProvisioningRequestForceClient) { + t.Helper() + prClient := fake.NewSimpleClientset() + ptClient := fake_kubernetes.NewSimpleClientset() + for _, pr := range prs { + if pr == nil { + continue + } + if _, err := prClient.AutoscalingV1beta1().ProvisioningRequests(pr.Namespace()).Create(ctx, pr.V1Beta1(), metav1.CreateOptions{}); err != nil { + t.Errorf("While adding a ProvisioningRequest: %s/%s to fake client, got error: %v", pr.Namespace(), pr.Name(), err) + } + for _, pd := range pr.PodTemplates() { + if _, err := ptClient.CoreV1().PodTemplates(pr.Namespace()).Create(ctx, pd, metav1.CreateOptions{}); err != nil { + t.Errorf("While adding a PodTemplate: %s/%s to fake client, got error: %v", pr.Namespace(), pd.Name, err) + } + } + } + prLister, err := newAllPRsLister(prClient, make(chan struct{})) + if err != nil { + t.Fatalf("Failed to create Provisioning Request lister. Error was: %v", err) + } + ptLister, err := newFakePodTemplatesLister(t, ptClient, make(chan struct{})) + if err != nil { + t.Fatalf("Failed to create Provisioning Request lister. Error was: %v", err) + } + return &ProvisioningRequestClient{ + client: prClient, + prLister: prLister, + ptLister: ptLister, + }, &FakeProvisioningRequestForceClient{ + client: prClient, + } +} + +// FakeProvisioningRequestForceClient that allows to skip cache. +type FakeProvisioningRequestForceClient struct { + client *fake.Clientset +} + +// ProvisioningRequests gets a specific ProvisioningRequest CR, skipping cache. +func (c *FakeProvisioningRequestForceClient) ProvisioningRequest(namespace, name string) (*v1beta1.ProvisioningRequest, error) { + ctx, cancel := context.WithTimeout(context.Background(), provisioningRequestClientCallTimeout) + defer cancel() + return c.client.AutoscalingV1beta1().ProvisioningRequests(namespace).Get(ctx, name, metav1.GetOptions{}) +} + +// newFakePodTemplatesLister creates a fake lister for the Pod Templates in the cluster. +func newFakePodTemplatesLister(t *testing.T, client kubernetes.Interface, channel <-chan struct{}) (v1.PodTemplateLister, error) { + t.Helper() + factory := informers.NewSharedInformerFactory(client, 1*time.Hour) + ptLister := factory.Core().V1().PodTemplates().Lister() + factory.Start(channel) + informersSynced := factory.WaitForCacheSync(channel) + for _, synced := range informersSynced { + if !synced { + return nil, fmt.Errorf("can't create Pod Template lister") + } + } + klog.V(2).Info("Successful initial Pod Template sync") + return ptLister, nil +} + +// ProvisioningRequestBetaForTests returns test ProvisioningRequest. +func ProvisioningRequestBetaForTests(namespace, name string) *provreqwrapper.ProvisioningRequest { + if namespace == "" { + namespace = "default" + } + podTemplates := []*apiv1.PodTemplate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: PodTemplateNameFromName(name), + Namespace: namespace, + }, + Template: apiv1.PodTemplateSpec{ + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{ + { + Name: "test-container", + Image: "test-image", + }, + }, + }, + }, + }, + } + v1Beta1PR := &v1beta1.ProvisioningRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1beta1.ProvisioningRequestSpec{ + ProvisioningClassName: "test-class", + PodSets: []v1beta1.PodSet{ + { + Count: 1, + PodTemplateRef: v1beta1.Reference{ + Name: podTemplates[0].Name, + }, + }, + }, + }, + Status: v1beta1.ProvisioningRequestStatus{ + ProvisioningClassDetails: map[string]v1beta1.Detail{}, + }, + } + + pr := provreqwrapper.NewV1Beta1ProvisioningRequest(v1Beta1PR, podTemplates) + return pr +} + +func PodTemplateNameFromName(name string) string { + return fmt.Sprintf("%s-pod-template", name) +}