From 8dbe398810147eb1f943f8314eb8a0a411992209 Mon Sep 17 00:00:00 2001 From: Shubham Rai Date: Thu, 20 Feb 2025 09:27:08 +0530 Subject: [PATCH] fixing operator suite test (#65) * fixing operator suite test * added test in makefile * added test as a step in github workflows * added test as a step in github workflows * added test at the end of lint * Running tests in all folder * removing race * added verbose output for go test * running raw go test * wip * wip * wip * test * wip * test * checking * test * test * use json.RawMessage for trigger metadata * use testEnv config for manager * move controller setup to BeforeSuite * resolve comments and cleanup --------- Co-authored-by: Ishaan Mittal Co-authored-by: Maanas M Singh <59373661+Maanas-23@users.noreply.github.com> Co-authored-by: Maanas M Singh --- .github/workflows/lint-and-test.yaml | 28 +++- Makefile | 15 ++ .../elasti/templates/elastiservice-crd.yaml | 7 +- operator/Makefile | 13 +- operator/api/v1alpha1/elastiservice_types.go | 8 +- .../api/v1alpha1/zz_generated.deepcopy.go | 54 ++++++- ...elasti.truefoundry.com_elastiservices.yaml | 7 +- operator/hack/boilerplate.go.txt | 15 ++ .../elastiservice_controller_test.go | 148 ++++++++++++++---- operator/internal/controller/suite_test.go | 81 +++++++--- pkg/Makefile | 22 +++ pkg/scaling/scalers/prometheus_scaler.go | 43 ++--- resolver/Makefile | 12 ++ 13 files changed, 352 insertions(+), 101 deletions(-) create mode 100644 operator/hack/boilerplate.go.txt create mode 100644 pkg/Makefile diff --git a/.github/workflows/lint-and-test.yaml b/.github/workflows/lint-and-test.yaml index b8a50e8b..4b3721e3 100644 --- a/.github/workflows/lint-and-test.yaml +++ b/.github/workflows/lint-and-test.yaml @@ -32,7 +32,7 @@ jobs: modules: ${{ fromJSON(needs.detect-modules.outputs.modules) }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: "0" - name: Setup go @@ -40,9 +40,33 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true + - name: Install dependencies + run: go mod tidy + working-directory: ${{ matrix.modules }} - name: golangci-lint ${{ matrix.modules }} uses: golangci/golangci-lint-action@v6 with: working-directory: ${{ matrix.modules }} args: -v --timeout 5m - version: v1.63.4 + version: v1.63.4 + test: + needs: detect-modules + runs-on: ubuntu-latest + strategy: + matrix: + modules: ${{ fromJSON(needs.detect-modules.outputs.modules) }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: "0" + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + - name: Install dependencies + run: go mod tidy + working-directory: ${{ matrix.modules }} + - name: test ${{ matrix.modules }} + run: cd ${{ matrix.modules }} && make test diff --git a/Makefile b/Makefile index e69b9b00..05cce71d 100644 --- a/Makefile +++ b/Makefile @@ -26,3 +26,18 @@ deploy: ## Deploy the operator and resolver undeploy: ## Undeploy the operator and resolver kubectl delete -f ./install.yaml +.PHONY: test +test: test-operator test-resolver test-pkg ## Run all tests + +.PHONY: test-operator +test-operator: ## Run operator tests + cd operator && make test + +.PHONY: test-resolver +test-resolver: ## Run resolver tests + cd resolver && make test + +.PHONY: test-pkg +test-pkg: ## Run pkg tests + cd pkg && make test + diff --git a/charts/elasti/templates/elastiservice-crd.yaml b/charts/elasti/templates/elastiservice-crd.yaml index 1cc4f4ab..4f156bef 100644 --- a/charts/elasti/templates/elastiservice-crd.yaml +++ b/charts/elasti/templates/elastiservice-crd.yaml @@ -74,7 +74,12 @@ spec: items: properties: metadata: - x-kubernetes-preserve-unknown-fields: true + description: |- + RawMessage is a raw encoded JSON value. + It implements [Marshaler] and [Unmarshaler] and can + be used to delay JSON decoding or precompute a JSON encoding. + format: byte + type: string type: type: string required: diff --git a/operator/Makefile b/operator/Makefile index ac9b7ed3..d11f44ff 100644 --- a/operator/Makefile +++ b/operator/Makefile @@ -26,17 +26,6 @@ all: build ##@ General -# The help target prints out all targets with their descriptions organized -# beneath their categories. The categories are represented by '##@' and the -# target descriptions by '##'. The awk command is responsible for reading the -# entire set of makefiles included in this invocation, looking for lines of the -# file as xyz: ## something, and then pretty-format the target and help. Then, -# if there's a line with ##@ something, that gets pretty-printed as a category. -# More info on the usage of ANSI control characters for terminal formatting: -# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters -# More info on the awk command: -# http://linuxcommand.org/lc3_adv_awk.php - .PHONY: help help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) @@ -61,7 +50,7 @@ vet: ## Run go vet against code. .PHONY: test test: manifests generate fmt vet envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -v $$(go list ./... | grep -v /e2e) -coverprofile cover.out # Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors. .PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up. diff --git a/operator/api/v1alpha1/elastiservice_types.go b/operator/api/v1alpha1/elastiservice_types.go index 286689b0..ef40b022 100644 --- a/operator/api/v1alpha1/elastiservice_types.go +++ b/operator/api/v1alpha1/elastiservice_types.go @@ -17,6 +17,8 @@ limitations under the License. package v1alpha1 import ( + "encoding/json" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -76,10 +78,8 @@ type ElastiServiceList struct { } type ScaleTrigger struct { - Type string `json:"type"` - // +kubebuilder:pruning:PreserveUnknownFields - // +kubebuilder:validation:Schemaless - Metadata any `json:"metadata,omitempty"` + Type string `json:"type"` + Metadata json.RawMessage `json:"metadata,omitempty"` } type AutoscalerSpec struct { diff --git a/operator/api/v1alpha1/zz_generated.deepcopy.go b/operator/api/v1alpha1/zz_generated.deepcopy.go index fa50636b..c384b132 100644 --- a/operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/operator/api/v1alpha1/zz_generated.deepcopy.go @@ -21,15 +21,31 @@ limitations under the License. package v1alpha1 import ( + "encoding/json" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AutoscalerSpec) DeepCopyInto(out *AutoscalerSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AutoscalerSpec. +func (in *AutoscalerSpec) DeepCopy() *AutoscalerSpec { + if in == nil { + return nil + } + out := new(AutoscalerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ElastiService) DeepCopyInto(out *ElastiService) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -87,6 +103,18 @@ func (in *ElastiServiceList) DeepCopyObject() runtime.Object { func (in *ElastiServiceSpec) DeepCopyInto(out *ElastiServiceSpec) { *out = *in out.ScaleTargetRef = in.ScaleTargetRef + if in.Triggers != nil { + in, out := &in.Triggers, &out.Triggers + *out = make([]ScaleTrigger, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Autoscaler != nil { + in, out := &in.Autoscaler, &out.Autoscaler + *out = new(AutoscalerSpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElastiServiceSpec. @@ -103,6 +131,10 @@ func (in *ElastiServiceSpec) DeepCopy() *ElastiServiceSpec { func (in *ElastiServiceStatus) DeepCopyInto(out *ElastiServiceStatus) { *out = *in in.LastReconciledTime.DeepCopyInto(&out.LastReconciledTime) + if in.LastScaledUpTime != nil { + in, out := &in.LastScaledUpTime, &out.LastScaledUpTime + *out = (*in).DeepCopy() + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElastiServiceStatus. @@ -129,3 +161,23 @@ func (in *ScaleTargetRef) DeepCopy() *ScaleTargetRef { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScaleTrigger) DeepCopyInto(out *ScaleTrigger) { + *out = *in + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScaleTrigger. +func (in *ScaleTrigger) DeepCopy() *ScaleTrigger { + if in == nil { + return nil + } + out := new(ScaleTrigger) + in.DeepCopyInto(out) + return out +} diff --git a/operator/config/crd/bases/elasti.truefoundry.com_elastiservices.yaml b/operator/config/crd/bases/elasti.truefoundry.com_elastiservices.yaml index 6ed83d55..2a40db39 100644 --- a/operator/config/crd/bases/elasti.truefoundry.com_elastiservices.yaml +++ b/operator/config/crd/bases/elasti.truefoundry.com_elastiservices.yaml @@ -73,7 +73,12 @@ spec: items: properties: metadata: - x-kubernetes-preserve-unknown-fields: true + description: |- + RawMessage is a raw encoded JSON value. + It implements [Marshaler] and [Unmarshaler] and can + be used to delay JSON decoding or precompute a JSON encoding. + format: byte + type: string type: type: string required: diff --git a/operator/hack/boilerplate.go.txt b/operator/hack/boilerplate.go.txt new file mode 100644 index 00000000..ff72ff2a --- /dev/null +++ b/operator/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +Copyright 2024. + +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. +*/ \ No newline at end of file diff --git a/operator/internal/controller/elastiservice_controller_test.go b/operator/internal/controller/elastiservice_controller_test.go index b2d29422..19067679 100644 --- a/operator/internal/controller/elastiservice_controller_test.go +++ b/operator/internal/controller/elastiservice_controller_test.go @@ -18,67 +18,161 @@ package controller import ( "context" + "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" elastiv1alpha1 "truefoundry/elasti/operator/api/v1alpha1" + + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" ) var _ = Describe("ElastiService Controller", func() { - Context("When reconciling a resource", func() { - const resourceName = "test-resource" + Context("When reconciling an elastiservice", func() { + const ( + resourceName = "test-elasti-service" + namespace = "elasti-test" + ) ctx := context.Background() - typeNamespacedName := types.NamespacedName{ + namespacedName := types.NamespacedName{ Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed + Namespace: namespace, } elastiservice := &elastiv1alpha1.ElastiService{} + deployment := &v1.Deployment{} + service := &corev1.Service{} BeforeEach(func() { - By("creating the custom resource for the Kind ElastiService") - err := k8sClient.Get(ctx, typeNamespacedName, elastiservice) - if err != nil && errors.IsNotFound(err) { - resource := &elastiv1alpha1.ElastiService{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", + By("creating a new Deployment") + deployment = &v1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: v1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": resourceName, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": resourceName, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "nginx:latest", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, deployment)).To(Succeed()) + + By("creating a new Service") + service = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "app": resourceName, + }, + Ports: []corev1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromInt32(80), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, service)).To(Succeed()) + + By("creating a new ElastiService resource") + elastiservice = &elastiv1alpha1.ElastiService{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Spec: elastiv1alpha1.ElastiServiceSpec{ + MinTargetReplicas: 1, + Service: resourceName, + ScaleTargetRef: elastiv1alpha1.ScaleTargetRef{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: resourceName, }, - // TODO(user): Specify other spec details if needed. - } - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + }, } + Expect(k8sClient.Create(ctx, elastiservice)).To(Succeed()) + + By("verifying all resources are created") + Expect(k8sClient.Get(ctx, namespacedName, elastiservice)).To(Succeed()) + Expect(k8sClient.Get(ctx, namespacedName, deployment)).To(Succeed()) + Expect(k8sClient.Get(ctx, namespacedName, service)).To(Succeed()) + }) AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. + By("Cleaning up all resources") + // Delete ElastiService resource := &elastiv1alpha1.ElastiService{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) - Expect(err).NotTo(HaveOccurred()) + err := k8sClient.Get(ctx, namespacedName, resource) + if err == nil { + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + } else if !errors.IsNotFound(err) { + fmt.Println("error deleting elastiService: ", err) + Fail("Error deleting elastiService") + } - By("Cleanup the specific resource instance ElastiService") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + // Delete Deployment + deploymentResource := &v1.Deployment{} + err = k8sClient.Get(ctx, namespacedName, deploymentResource) + if err == nil { + Expect(k8sClient.Delete(ctx, deploymentResource)).To(Succeed()) + } else if !errors.IsNotFound(err) { + fmt.Println("error deleting deployment: ", err) + Fail("Error deleting deployment") + } + + // Delete Service + serviceResource := &corev1.Service{} + err = k8sClient.Get(ctx, namespacedName, serviceResource) + if err == nil { + Expect(k8sClient.Delete(ctx, serviceResource)).To(Succeed()) + } else if !errors.IsNotFound(err) { + fmt.Println("error deleting service: ", err) + Fail("Error deleting service") + } }) + It("should successfully reconcile the resource", func() { By("Reconciling the created resource") - controllerReconciler := &ElastiServiceReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - } + By("getting the deployment") + Expect(k8sClient.Get(mgrCtx, namespacedName, deployment)).To(Succeed()) - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, + By("reconciling the resource") + _, err := controllerReconciler.Reconcile(mgrCtx, reconcile.Request{ + NamespacedName: namespacedName, }) Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. }) }) }) diff --git a/operator/internal/controller/suite_test.go b/operator/internal/controller/suite_test.go index 61ccea70..237d2b77 100644 --- a/operator/internal/controller/suite_test.go +++ b/operator/internal/controller/suite_test.go @@ -17,14 +17,25 @@ limitations under the License. package controller import ( + "context" "fmt" "path/filepath" "runtime" + "sync" "testing" + "time" + "truefoundry/elasti/operator/internal/crddirectory" + "truefoundry/elasti/operator/internal/informer" + + uberZap "go.uber.org/zap" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + ctrl "sigs.k8s.io/controller-runtime" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -33,41 +44,37 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" elastiv1alpha1 "truefoundry/elasti/operator/api/v1alpha1" - //+kubebuilder:scaffold:imports ) -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment +var namespace = "elasti-test" + +var mgrCtx context.Context +var mgrCancel context.CancelFunc +var informerManager *informer.Manager +var controllerReconciler *ElastiServiceReconciler func TestControllers(t *testing.T) { RegisterFailHandler(Fail) - - RunSpecs(t, "Controller Suite") + RunSpecs(t, "Controller Suite", Label("controller")) } var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + zapLogger := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)) + logf.SetLogger(zapLogger) By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - - // The BinaryAssetsDirectory is only required if you want to run the tests directly - // without call the makefile target test. If not informed it will look for the - // default path defined in controller-runtime which is /usr/local/kubebuilder/. - // Note that you must have the required binaries setup under the bin directory to perform - // the tests directly. When we run make test it will be setup and used automatically. + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + ControlPlaneStopTimeout: 1 * time.Minute, BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)), } var err error - // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) @@ -80,11 +87,47 @@ var _ = BeforeSuite(func() { k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) + err = k8sClient.Create(context.Background(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + }) + Expect(err).NotTo(HaveOccurred()) + + informerManager = informer.NewInformerManager(uberZap.NewExample(), cfg) + informerManager.Start() + crddirectory.InitDirectory(uberZap.NewExample()) + mgrCtx, mgrCancel = context.WithCancel(context.Background()) + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: k8sClient.Scheme(), + }) + Expect(err).NotTo(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = mgr.Start(mgrCtx) + Expect(err).NotTo(HaveOccurred()) + gexec.KillAndWait(5 * time.Second) + + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) + }() + + controllerReconciler = &ElastiServiceReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + Logger: uberZap.NewExample(), + InformerManager: informerManager, + SwitchModeLocks: sync.Map{}, + InformerStartLocks: sync.Map{}, + ReconcileLocks: sync.Map{}, + } + Expect(controllerReconciler.SetupWithManager(mgr)).To(Succeed()) }) var _ = AfterSuite(func() { - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) + informerManager.Stop() + mgrCancel() }) diff --git a/pkg/Makefile b/pkg/Makefile new file mode 100644 index 00000000..d7b70f38 --- /dev/null +++ b/pkg/Makefile @@ -0,0 +1,22 @@ +.PHONY: help +help: + @echo "Available targets:" + @awk '/^[a-zA-Z0-9_-]+:.*?##/ { \ + nb = index($$0, "##"); \ + target = substr($$0, 1, nb - 2); \ + helpMsg = substr($$0, nb + 3); \ + printf " %-15s %s\n", target, helpMsg; \ + }' $(MAKEFILE_LIST) | column -s ':' -t + +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: fmt vet ## Run tests. + go test ./... -race + diff --git a/pkg/scaling/scalers/prometheus_scaler.go b/pkg/scaling/scalers/prometheus_scaler.go index 5c5bbcbb..092701c8 100644 --- a/pkg/scaling/scalers/prometheus_scaler.go +++ b/pkg/scaling/scalers/prometheus_scaler.go @@ -21,9 +21,9 @@ type prometheusScaler struct { } type prometheusMetadata struct { - ServerAddress string - Query string - Threshold float64 + ServerAddress string `json:"serverAddress"` + Query string `json:"query"` + Threshold float64 `json:"threshold,string"` } var promQueryResponse struct { @@ -37,7 +37,7 @@ var promQueryResponse struct { } `json:"data"` } -func NewPrometheusScaler(metadata any) (Scaler, error) { +func NewPrometheusScaler(metadata json.RawMessage) (Scaler, error) { parsedMetadata, err := parsePrometheusMetadata(metadata) if err != nil { return nil, fmt.Errorf("error creating prometheus scaler: %w", err) @@ -53,38 +53,13 @@ func NewPrometheusScaler(metadata any) (Scaler, error) { }, nil } -func parsePrometheusMetadata(metadata any) (*prometheusMetadata, error) { - // TODO implement a generic way to parse metadata - metadataMap, ok := metadata.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("error parsing prometheus metadata: expected map[string]interface{}, got %T", metadata) - } - - serverAddress, ok := metadataMap["serverAddress"].(string) - if !ok || serverAddress == "" { - return nil, fmt.Errorf("missing serverAddress") - } - - query, ok := metadataMap["query"].(string) - if !ok || query == "" { - return nil, fmt.Errorf("missing query") - } - - thresholdStr, ok := metadataMap["threshold"].(string) - if !ok { - return nil, fmt.Errorf("missing threshold") - } - - threshold, err := strconv.ParseFloat(thresholdStr, 64) +func parsePrometheusMetadata(jsonMetadata json.RawMessage) (*prometheusMetadata, error) { + metadata := &prometheusMetadata{} + err := json.Unmarshal(jsonMetadata, metadata) if err != nil { - return nil, fmt.Errorf("failed to parse threshold: %w", err) + return nil, fmt.Errorf("failed to parse metadata: %w", err) } - - return &prometheusMetadata{ - ServerAddress: serverAddress, - Query: query, - Threshold: threshold, - }, nil + return metadata, nil } func (s *prometheusScaler) executePromQuery(ctx context.Context) (float64, error) { diff --git a/resolver/Makefile b/resolver/Makefile index fb788498..f36d97d2 100644 --- a/resolver/Makefile +++ b/resolver/Makefile @@ -14,6 +14,18 @@ help: printf " %-15s %s\n", target, helpMsg; \ }' $(MAKEFILE_LIST) | column -s ':' -t +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... + +.PHONY: test +test: fmt vet ## Run tests. + go test ./... -race + .PHONY: run run: ## Run resolver locally go run ./cmd/