add the metrics trait

This commit is contained in:
Ryan Zhang
2020-08-26 18:49:42 -07:00
parent ef0d974c16
commit 2478c1a9ae
19 changed files with 2463 additions and 16 deletions

1
.gitignore vendored
View File

@@ -31,6 +31,7 @@ vendor/
.vscode
pkg/test/vela
config/crd/bases
tmp/
# Dashboard

View File

@@ -91,7 +91,6 @@ core-deploy: manifests
manifests: controller-gen
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# Generate code
generate: controller-gen
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."

View File

@@ -0,0 +1,36 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1alpha1 contains API Schema definitions for the standard v1alpha1 API group
// +kubebuilder:object:generate=true
// +groupName=standard.oam.dev
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: "standard.oam.dev", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@@ -0,0 +1,114 @@
/*
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 (
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/oam-kubernetes-runtime/pkg/oam"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// MetricsTraitSpec defines the desired state of MetricsTrait
type MetricsTraitSpec struct {
// An endpoint to be monitored by a ServiceMonitor.
ScrapeService ScapeServiceEndPoint `json:"scrapeService"`
// WorkloadReference to the workload whose metrics needs to be exposed
WorkloadReference runtimev1alpha1.TypedReference `json:"workloadRef,omitempty"`
}
// ScapeServiceEndPoint defines a scrapeable endpoint serving Prometheus metrics.
type ScapeServiceEndPoint struct {
// The format of the metrics data,
// The default and only supported format is "prometheus" for now
Format string `json:"format,omitempty"`
// Number or name of the port to access on the pods targeted by the service.
// When this field has value implies that we need to create a service for the workload
// Mutually exclusive with port.
TargetPort intstr.IntOrString `json:"port,omitempty"`
// Route service traffic to pods with label keys and values matching this
// The default is the labels in the workload
// Mutually exclusive with port.
TargetSelector map[string]string `json:"selector,omitempty"`
// HTTP path to scrape for metrics.
// default is /metrics
// +optional
Path string `json:"path,omitempty"`
// Scheme at which metrics should be scraped
// The default and only supported scheme is "http"
// +optional
Scheme string `json:"scheme,omitempty"`
// The default is true
// +optional
Enabled *bool `json:"enabled,omitempty"`
}
// MetricsTraitStatus defines the observed state of MetricsTrait
type MetricsTraitStatus struct {
runtimev1alpha1.ConditionedStatus `json:",inline"`
// ServiceMonitorNames managed by this trait
ServiceMonitorNames []string `json:"serviceMonitorName,omitempty"`
}
// +kubebuilder:object:root=true
// MetricsTrait is the Schema for the metricstraits API
// +kubebuilder:resource:categories={oam}
// +kubebuilder:subresource:status
type MetricsTrait struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MetricsTraitSpec `json:"spec"`
Status MetricsTraitStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// MetricsTraitList contains a list of MetricsTrait
type MetricsTraitList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MetricsTrait `json:"items"`
}
func init() {
SchemeBuilder.Register(&MetricsTrait{}, &MetricsTraitList{})
}
var _ oam.Trait = &MetricsTrait{}
func (in *MetricsTrait) SetConditions(c ...runtimev1alpha1.Condition) {
in.Status.SetConditions(c...)
}
func (in *MetricsTrait) GetCondition(c runtimev1alpha1.ConditionType) runtimev1alpha1.Condition {
return in.Status.GetCondition(c)
}
// GetWorkloadReference of this ManualScalerTrait.
func (tr *MetricsTrait) GetWorkloadReference() runtimev1alpha1.TypedReference {
return tr.Spec.WorkloadReference
}
// SetWorkloadReference of this ManualScalerTrait.
func (tr *MetricsTrait) SetWorkloadReference(r runtimev1alpha1.TypedReference) {
tr.Spec.WorkloadReference = r
}

View File

@@ -0,0 +1,150 @@
// +build !ignore_autogenerated
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by controller-gen. DO NOT EDIT.
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MetricsTrait) DeepCopyInto(out *MetricsTrait) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsTrait.
func (in *MetricsTrait) DeepCopy() *MetricsTrait {
if in == nil {
return nil
}
out := new(MetricsTrait)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MetricsTrait) 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 *MetricsTraitList) DeepCopyInto(out *MetricsTraitList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]MetricsTrait, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsTraitList.
func (in *MetricsTraitList) DeepCopy() *MetricsTraitList {
if in == nil {
return nil
}
out := new(MetricsTraitList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MetricsTraitList) 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 *MetricsTraitSpec) DeepCopyInto(out *MetricsTraitSpec) {
*out = *in
in.ScrapeService.DeepCopyInto(&out.ScrapeService)
out.WorkloadReference = in.WorkloadReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsTraitSpec.
func (in *MetricsTraitSpec) DeepCopy() *MetricsTraitSpec {
if in == nil {
return nil
}
out := new(MetricsTraitSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MetricsTraitStatus) DeepCopyInto(out *MetricsTraitStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
if in.ServiceMonitorNames != nil {
in, out := &in.ServiceMonitorNames, &out.ServiceMonitorNames
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsTraitStatus.
func (in *MetricsTraitStatus) DeepCopy() *MetricsTraitStatus {
if in == nil {
return nil
}
out := new(MetricsTraitStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ScapeServiceEndPoint) DeepCopyInto(out *ScapeServiceEndPoint) {
*out = *in
out.TargetPort = in.TargetPort
if in.TargetSelector != nil {
in, out := &in.TargetSelector, &out.TargetSelector
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Enabled != nil {
in, out := &in.Enabled, &out.Enabled
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScapeServiceEndPoint.
func (in *ScapeServiceEndPoint) DeepCopy() *ScapeServiceEndPoint {
if in == nil {
return nil
}
out := new(ScapeServiceEndPoint)
in.DeepCopyInto(out)
return out
}

View File

@@ -15,8 +15,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"github.com/crossplane/oam-kubernetes-runtime/apis/core"
controller "github.com/crossplane/oam-kubernetes-runtime/pkg/controller/v1alpha2"
webhook "github.com/crossplane/oam-kubernetes-runtime/pkg/webhook/v1alpha2"
oamcontroller "github.com/crossplane/oam-kubernetes-runtime/pkg/controller"
oamv1alpha2 "github.com/crossplane/oam-kubernetes-runtime/pkg/controller/v1alpha2"
oamwebhook "github.com/crossplane/oam-kubernetes-runtime/pkg/webhook/v1alpha2"
)
var scheme = runtime.NewScheme()
@@ -34,6 +35,7 @@ func main() {
var certDir string
var webhookPort int
var useWebhook bool
var controllerArgs oamcontroller.Args
flag.BoolVar(&useWebhook, "use-webhook", false, "Enable Admission Webhook")
flag.StringVar(&certDir, "webhook-cert-dir", "/k8s-webhook-server/serving-certs", "Admission webhook cert/key dir.")
@@ -44,6 +46,8 @@ func main() {
flag.StringVar(&logFilePath, "log-file-path", "", "The address the metric endpoint binds to.")
flag.IntVar(&logRetainDate, "log-retain-date", 7, "The number of days of logs history to retain.")
flag.BoolVar(&logCompress, "log-compress", true, "Enable compression on the rotated logs.")
flag.IntVar(&controllerArgs.RevisionLimit, "revision-limit", 50,
"RevisionLimit is the maximum number of revisions that will be maintained. The default value is 50.")
flag.Parse()
// setup logging
@@ -63,12 +67,12 @@ func main() {
o.DestWritter = w
}))
oamLog := ctrl.Log.WithName("vela-core")
oamLog := ctrl.Log.WithName("oam-kubernetes-runtime")
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "vela-core",
LeaderElectionID: "oam-kubernetes-runtime",
Port: webhookPort,
CertDir: certDir,
})
@@ -79,10 +83,10 @@ func main() {
if useWebhook {
oamLog.Info("OAM webhook enabled, will serving at :" + strconv.Itoa(webhookPort))
webhook.Add(mgr)
oamwebhook.Add(mgr)
}
if err = controller.Setup(mgr, logging.NewLogrLogger(oamLog)); err != nil {
if err = oamv1alpha2.Setup(mgr, controllerArgs, logging.NewLogrLogger(oamLog)); err != nil {
oamLog.Error(err, "unable to setup the oam core controller")
os.Exit(1)
}

76
config/rbac/role.yaml Normal file
View File

@@ -0,0 +1,76 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- ""
resources:
- events
verbs:
- create
- get
- list
- patch
- update
- apiGroups:
- core.oam.dev
resources:
- '*'
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- core.oam.dev
resources:
- '*/status'
verbs:
- get
- apiGroups:
- monitoring.coreos.com
resources:
- '*'
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- monitoring.coreos.com
resources:
- '*/status'
verbs:
- get
- patch
- update
- apiGroups:
- standard.oam.dev
resources:
- metricstraits
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- standard.oam.dev
resources:
- metricstraits/status
verbs:
- get
- patch
- update

View File

@@ -0,0 +1,53 @@
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
creationTimestamp: null
name: mutating-webhook-configuration
webhooks:
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /mutate-standard-oam-dev-v1alpha1-metricstrait
failurePolicy: Fail
name: mmetricstrait.kb.io
rules:
- apiGroups:
- standard.oam.dev
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- metricstraits
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
creationTimestamp: null
name: validating-webhook-configuration
webhooks:
- clientConfig:
caBundle: Cg==
service:
name: webhook-service
namespace: system
path: /validate-standard-oam-dev-v1alpha1-metricstrait
failurePolicy: Fail
name: vmetricstrait.kb.io
rules:
- apiGroups:
- standard.oam.dev
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
- DELETE
resources:
- metricstraits

20
go.mod
View File

@@ -4,24 +4,26 @@ go 1.13
require (
cuelang.org/go v0.2.2
github.com/crossplane/crossplane-runtime v0.8.0
github.com/crossplane/oam-kubernetes-runtime v0.0.8
github.com/coreos/prometheus-operator v0.41.1
github.com/crossplane/crossplane-runtime v0.9.0
github.com/crossplane/oam-kubernetes-runtime v0.0.9
github.com/gertd/go-pluralize v0.1.7
github.com/ghodss/yaml v1.0.0
github.com/gin-gonic/gin v1.6.3
github.com/go-logr/logr v0.1.0
github.com/google/go-cmp v0.5.2
github.com/google/go-github/v32 v32.1.0
github.com/gosuri/uitable v0.0.4
github.com/oam-dev/catalog/traits/metricstrait v0.0.0-20200826071236-d96c1d64e221
github.com/onsi/ginkgo v1.11.0
github.com/onsi/gomega v1.8.1
github.com/pkg/errors v0.9.1
github.com/satori/go.uuid v1.2.0
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.6.1
go.uber.org/zap v1.10.0
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55
go.uber.org/zap v1.13.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gotest.tools v2.2.0+incompatible
helm.sh/helm/v3 v3.2.4
@@ -29,9 +31,13 @@ require (
k8s.io/apiextensions-apiserver v0.18.2
k8s.io/apimachinery v0.18.6
k8s.io/cli-runtime v0.18.6
k8s.io/client-go v0.18.6
k8s.io/client-go v12.0.0+incompatible
k8s.io/klog v1.0.0
k8s.io/kubectl v0.18.6 // indirect
k8s.io/utils v0.0.0-20200414100711-2df71ebbae66
rsc.io/letsencrypt v0.0.3 // indirect
sigs.k8s.io/controller-runtime v0.6.0
)
// clint-go had a buggy release, https://github.com/kubernetes/client-go/issues/749
replace k8s.io/client-go => k8s.io/client-go v0.18.6

989
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,307 @@
/*
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 metrics
import (
"context"
"fmt"
"reflect"
monitoring "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1"
cpv1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/event"
oamutil "github.com/crossplane/oam-kubernetes-runtime/pkg/oam/util"
"github.com/go-logr/logr"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/cloud-native-application/rudrx/api/v1alpha1"
)
const (
errApplyServiceMonitor = "failed to apply the service monitor"
errLocatingService = "failed to locate any the services"
servicePort = 4848
)
var (
serviceMonitorKind = reflect.TypeOf(monitoring.ServiceMonitor{}).Name()
serviceMonitorAPIVersion = monitoring.SchemeGroupVersion.String()
serviceKind = reflect.TypeOf(corev1.Service{}).Name()
serviceAPIVersion = corev1.SchemeGroupVersion.String()
trueVar = true
)
var (
// oamServiceLabel is the pre-defined labels for any serviceMonitor
// created by the MetricsTrait, prometheus operator listens on this
oamServiceLabel = map[string]string{
"k8s-app": "oam",
"controller": "metricsTrait",
}
// serviceMonitorNSName is the name of the namespace in which the serviceMonitor resides
// it must be the same that the prometheus operator is listening to
serviceMonitorNSName = "oam-monitoring"
)
// MetricsTraitReconciler reconciles a MetricsTrait object
type MetricsTraitReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
record event.Recorder
}
// +kubebuilder:rbac:groups=standard.oam.dev,resources=metricstraits,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=standard.oam.dev,resources=metricstraits/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=*,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=*/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core.oam.dev,resources=*,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core.oam.dev,resources=*/status,verbs=get;
// +kubebuilder:rbac:groups="",resources=events,verbs=get;list;create;update;patch
func (r *MetricsTraitReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
mLog := r.Log.WithValues("metricstrait", req.NamespacedName)
mLog.Info("Reconcile metricstrait trait")
// fetch the trait
var metricsTrait v1alpha1.MetricsTrait
if err := r.Get(ctx, req.NamespacedName, &metricsTrait); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
mLog.Info("Get the metricsTrait trait",
"metrics end point", metricsTrait.Spec.ScrapeService,
"workload reference", metricsTrait.Spec.WorkloadReference,
"labels", metricsTrait.GetLabels())
// find the resource object to record the event to, default is the parent appConfig.
eventObj, err := oamutil.LocateParentAppConfig(ctx, r.Client, &metricsTrait)
if eventObj == nil {
// fallback to workload itself
mLog.Error(err, "add events to metricsTrait itself", "name", metricsTrait.Name)
eventObj = &metricsTrait
}
if metricsTrait.Spec.ScrapeService.Enabled != nil && !*metricsTrait.Spec.ScrapeService.Enabled {
r.record.Event(eventObj, event.Normal("Metrics Trait disabled", "no op"))
r.gcOrphanServiceMonitor(ctx, mLog, &metricsTrait)
return ctrl.Result{}, oamutil.PatchCondition(ctx, r, &metricsTrait, cpv1alpha1.ReconcileSuccess())
}
// Fetch the workload instance to which we want to expose metrics
workload, err := oamutil.FetchWorkload(ctx, r, mLog, &metricsTrait)
if err != nil {
mLog.Error(err, "Error while fetching the workload", "workload reference",
metricsTrait.GetWorkloadReference())
r.record.Event(eventObj, event.Warning(errLocatingService, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &metricsTrait,
cpv1alpha1.ReconcileError(errors.Wrap(err, errLocatingService)))
}
// try to see if the workload already has services as child resources
serviceLabel, err := r.fetchServicesLabel(ctx, mLog, workload, metricsTrait.Spec.ScrapeService.TargetPort)
if err != nil && !apierrors.IsNotFound(err) {
r.record.Event(eventObj, event.Warning(errLocatingService, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &metricsTrait,
cpv1alpha1.ReconcileError(errors.Wrap(err, errLocatingService)))
} else if serviceLabel == nil {
// TODO: use podMonitor instead?
// no service with the targetPort found, we will create a service that talks to the targetPort
serviceLabel, err = r.createService(ctx, mLog, workload, &metricsTrait)
if err != nil {
r.record.Event(eventObj, event.Warning(errLocatingService, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &metricsTrait,
cpv1alpha1.ReconcileError(errors.Wrap(err, errLocatingService)))
}
}
// construct the serviceMonitor that hooks the service to the prometheus server
serviceMonitor := constructServiceMonitor(&metricsTrait, serviceLabel)
// server side apply the serviceMonitor, only the fields we set are touched
applyOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(metricsTrait.GetUID())}
if err := r.Patch(ctx, serviceMonitor, client.Apply, applyOpts...); err != nil {
mLog.Error(err, "Failed to apply to serviceMonitor")
r.record.Event(eventObj, event.Warning(errApplyServiceMonitor, err))
return oamutil.ReconcileWaitResult,
oamutil.PatchCondition(ctx, r, &metricsTrait,
cpv1alpha1.ReconcileError(errors.Wrap(err, errApplyServiceMonitor)))
}
r.record.Event(eventObj, event.Normal("ServiceMonitor created",
fmt.Sprintf("successfully server side patched a serviceMonitor `%s`", serviceMonitor.Name)))
r.gcOrphanServiceMonitor(ctx, mLog, &metricsTrait)
return ctrl.Result{}, oamutil.PatchCondition(ctx, r, &metricsTrait, cpv1alpha1.ReconcileSuccess())
}
// fetch the label of the service that is associated with the workload
func (r *MetricsTraitReconciler) fetchServicesLabel(ctx context.Context, mLog logr.Logger,
workload *unstructured.Unstructured, targetPort intstr.IntOrString) (map[string]string, error) {
// Fetch the child resources list from the corresponding workload
resources, err := oamutil.FetchWorkloadChildResources(ctx, mLog, r, workload)
if err != nil {
if !apierrors.IsNotFound(err) {
mLog.Error(err, "Error while fetching the workload child resources", "workload kind", workload.GetKind(),
"workload name", workload.GetName())
}
return nil, err
}
// find the service that has the port
for _, childRes := range resources {
if childRes.GetAPIVersion() == serviceAPIVersion && childRes.GetKind() == serviceKind {
ports, _, _ := unstructured.NestedSlice(childRes.Object, "spec", "ports")
for _, port := range ports {
servicePort, _ := port.(corev1.ServicePort)
if servicePort.TargetPort == targetPort {
return childRes.GetLabels(), nil
}
}
}
}
return nil, nil
}
// create a service that targets the exposed workload pod
func (r *MetricsTraitReconciler) createService(ctx context.Context, mLog logr.Logger, workload *unstructured.Unstructured,
metricsTrait *v1alpha1.MetricsTrait) (map[string]string, error) {
oamService := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: serviceKind,
APIVersion: serviceAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "oam-" + workload.GetName(),
Namespace: workload.GetNamespace(),
Labels: oamServiceLabel,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
},
}
// assign selector
if len(metricsTrait.Spec.ScrapeService.TargetSelector) == 0 {
// default is that we assumed that the pods have the same label as the workload
// we might be able to find the podSpec label but it is more complicated
oamService.Spec.Selector = workload.GetLabels()
} else {
oamService.Spec.Selector = metricsTrait.Spec.ScrapeService.TargetSelector
}
oamService.Spec.Ports = []corev1.ServicePort{
{
Port: servicePort,
TargetPort: metricsTrait.Spec.ScrapeService.TargetPort,
Protocol: corev1.ProtocolTCP,
},
}
// server side apply the service, only the fields we set are touched
applyOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(metricsTrait.GetUID())}
if err := r.Patch(ctx, oamService, client.Apply, applyOpts...); err != nil {
mLog.Error(err, "Failed to apply to service")
return nil, err
}
return oamServiceLabel, nil
}
// remove all service monitors that are no longer used
func (r *MetricsTraitReconciler) gcOrphanServiceMonitor(ctx context.Context, mLog logr.Logger,
metricsTrait *v1alpha1.MetricsTrait) {
var gcCandidates []string
copy(metricsTrait.Status.ServiceMonitorNames, gcCandidates)
if metricsTrait.Spec.ScrapeService.Enabled != nil && !*metricsTrait.Spec.ScrapeService.Enabled {
// initialize it to be an empty list, gc everything
metricsTrait.Status.ServiceMonitorNames = []string{}
} else {
// re-initialize to the current service monitor
metricsTrait.Status.ServiceMonitorNames = []string{metricsTrait.Name}
}
for _, smn := range gcCandidates {
if smn != metricsTrait.Name {
if err := r.Delete(ctx, &monitoring.ServiceMonitor{
TypeMeta: metav1.TypeMeta{
Kind: serviceMonitorKind,
APIVersion: serviceMonitorAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: smn,
Namespace: metricsTrait.GetNamespace(),
},
}, client.GracePeriodSeconds(10)); err != nil {
mLog.Error(err, "Failed to delete serviceMonitor", "name", smn, "error", err)
// add it back
metricsTrait.Status.ServiceMonitorNames = append(metricsTrait.Status.ServiceMonitorNames, smn)
}
}
}
}
// construct a serviceMonitor given a metrics trait along with a label selector pointing to the underlying service
func constructServiceMonitor(metricsTrait *v1alpha1.MetricsTrait,
serviceLabels map[string]string) *monitoring.ServiceMonitor {
return &monitoring.ServiceMonitor{
TypeMeta: metav1.TypeMeta{
Kind: serviceMonitorKind,
APIVersion: serviceMonitorAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: metricsTrait.Name,
Namespace: serviceMonitorNSName,
Labels: oamServiceLabel,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: metricsTrait.GetObjectKind().GroupVersionKind().GroupVersion().String(),
Kind: metricsTrait.GetObjectKind().GroupVersionKind().Kind,
UID: metricsTrait.GetUID(),
Name: metricsTrait.GetName(),
Controller: &trueVar,
BlockOwnerDeletion: &trueVar,
},
},
},
Spec: monitoring.ServiceMonitorSpec{
Selector: metav1.LabelSelector{
MatchLabels: serviceLabels,
},
// we assumed that the service is in the same namespace as the trait
NamespaceSelector: monitoring.NamespaceSelector{
MatchNames: []string{metricsTrait.Namespace},
},
Endpoints: []monitoring.Endpoint{
{
TargetPort: &metricsTrait.Spec.ScrapeService.TargetPort,
Path: metricsTrait.Spec.ScrapeService.Path,
Scheme: metricsTrait.Spec.ScrapeService.Scheme,
},
},
},
}
}
func (r *MetricsTraitReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor("MetricsTrait")).
WithAnnotations("controller", "metricsTrait")
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.MetricsTrait{}).
Owns(&monitoring.ServiceMonitor{}).
Complete(r)
}

View File

@@ -0,0 +1,216 @@
package metrics
import (
"context"
"reflect"
"time"
monitoringv1 "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1"
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/oam-kubernetes-runtime/pkg/oam/util"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/cloud-native-application/rudrx/api/v1alpha1"
)
var (
metricsTraitKind = reflect.TypeOf(v1alpha1.MetricsTrait{}).Name()
metricsTraitAPIVersion = v1alpha1.SchemeGroupVersion.String()
deploymentKind = reflect.TypeOf(appsv1.Deployment{}).Name()
deploymentAPIVersion = appsv1.SchemeGroupVersion.String()
)
var _ = Describe("Metrics Trait Integration Test", func() {
// common var init
ctx := context.Background()
namespaceName := "metricstrait-integration-test"
traitLabel := map[string]string{"trait": "metricsTraitBase"}
deployLabel := map[string]string{"standard.oam.dev": "oam-test-deployment"}
podPort := 8080
targetPort := intstr.FromInt(podPort)
metricsPath := "/notMetrics"
scheme := "http"
var ns corev1.Namespace
var metricsTraitBase v1alpha1.MetricsTrait
var workloadBase appsv1.Deployment
BeforeEach(func() {
logf.Log.Info("[TEST] Set up resources before an integration test")
ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespaceName,
},
}
By("Create the Namespace for test")
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
metricsTraitBase = v1alpha1.MetricsTrait{
TypeMeta: metav1.TypeMeta{
Kind: metricsTraitKind,
APIVersion: metricsTraitAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
Labels: traitLabel,
},
Spec: v1alpha1.MetricsTraitSpec{
ScrapeService: v1alpha1.ScapeServiceEndPoint{
TargetPort: targetPort,
Path: metricsPath,
Scheme: scheme,
Enabled: &trueVar,
},
WorkloadReference: runtimev1alpha1.TypedReference{
APIVersion: deploymentAPIVersion,
Kind: deploymentKind,
},
},
}
workloadBase = appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: deploymentKind,
APIVersion: deploymentAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
Labels: deployLabel,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: deployLabel,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: deployLabel,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "container-name",
Image: "alpine",
ImagePullPolicy: corev1.PullNever,
Command: []string{"containerCommand"},
Args: []string{"containerArguments"},
Ports: []corev1.ContainerPort{
{
ContainerPort: int32(podPort),
},
},
},
},
},
},
},
}
})
AfterEach(func() {
// Control-runtime test ENV has a bug that can't delete resources like deployment/namespaces
// We have to use different names to segregate between tests
logf.Log.Info("[TEST] Clean up resources after an integration test")
})
It("Test with deployment as workloadBase without selector", func() {
testName := "deploy-without-selector"
By("Create the deployment as the workloadBase")
workload := workloadBase
workload.Name = testName + "-workload"
Expect(k8sClient.Create(ctx, &workload)).ToNot(HaveOccurred())
By("Create the metrics trait pointing to the workloadBase")
metricsTrait := metricsTraitBase
metricsTrait.Name = testName + "-trait"
metricsTrait.Spec.WorkloadReference.Name = workload.Name
Expect(k8sClient.Create(ctx, &metricsTrait)).ToNot(HaveOccurred())
By("Check that we have created the service")
createdService := corev1.Service{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: "oam-" + workload.GetName()},
&createdService)
},
time.Second*10, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created service", "service ports", createdService.Spec.Ports)
Expect(createdService.GetNamespace()).Should(Equal(namespaceName))
Expect(createdService.Labels).Should(Equal(oamServiceLabel))
Expect(len(createdService.Spec.Ports)).Should(Equal(1))
Expect(createdService.Spec.Ports[0].Port).Should(BeEquivalentTo(servicePort))
Expect(createdService.Spec.Selector).Should(Equal(deployLabel))
By("Check that we have created the serviceMonitor in the pre-defined namespaceName")
var serviceMonitor monitoringv1.ServiceMonitor
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: serviceMonitorNSName, Name: metricsTrait.GetName()},
&serviceMonitor)
},
time.Second*5, time.Millisecond*50).Should(BeNil())
logf.Log.Info("[TEST] Get the created serviceMonitor", "service end ports", serviceMonitor.Spec.Endpoints)
Expect(serviceMonitor.GetNamespace()).Should(Equal(serviceMonitorNSName))
Expect(serviceMonitor.Spec.Selector.MatchLabels).Should(Equal(oamServiceLabel))
Expect(serviceMonitor.Spec.Selector.MatchExpressions).Should(BeNil())
Expect(serviceMonitor.Spec.NamespaceSelector.MatchNames).Should(Equal([]string{metricsTrait.Namespace}))
Expect(serviceMonitor.Spec.NamespaceSelector.Any).Should(BeFalse())
Expect(len(serviceMonitor.Spec.Endpoints)).Should(Equal(1))
Expect(serviceMonitor.Spec.Endpoints[0].Port).Should(BeEmpty())
Expect(*serviceMonitor.Spec.Endpoints[0].TargetPort).Should(BeEquivalentTo(targetPort))
Expect(serviceMonitor.Spec.Endpoints[0].Scheme).Should(Equal(scheme))
Expect(serviceMonitor.Spec.Endpoints[0].Path).Should(Equal(metricsPath))
})
It("Test with deployment as workloadBase selector", func() {
testName := "deploy-with-selector"
By("Create the deployment as the workloadBase")
workload := workloadBase.DeepCopy()
workload.Name = testName + "-workload"
Expect(k8sClient.Create(ctx, workload)).ToNot(HaveOccurred())
By("Create the metrics trait pointing to the workloadBase")
podSelector := map[string]string{"podlabel": "goodboy"}
metricsTrait := metricsTraitBase
metricsTrait.Name = testName + "-trait"
metricsTrait.Spec.WorkloadReference.Name = workload.Name
metricsTrait.Spec.ScrapeService.TargetSelector = podSelector
Expect(k8sClient.Create(ctx, &metricsTrait)).ToNot(HaveOccurred())
By("Check that we have created the service")
createdService := corev1.Service{}
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns.Name, Name: "oam-" + workload.GetName()},
&createdService)
},
time.Second*10, time.Millisecond*500).Should(BeNil())
logf.Log.Info("[TEST] Get the created service", "service ports", createdService.Spec.Ports)
Expect(createdService.Labels).Should(Equal(oamServiceLabel))
Expect(createdService.Spec.Selector).Should(Equal(podSelector))
By("Check that we have created the serviceMonitor in the pre-defined namespaceName")
var serviceMonitor monitoringv1.ServiceMonitor
Eventually(
func() error {
return k8sClient.Get(ctx,
types.NamespacedName{Namespace: serviceMonitorNSName, Name: metricsTrait.GetName()},
&serviceMonitor)
},
time.Second*5, time.Millisecond*50).Should(BeNil())
logf.Log.Info("[TEST] Get the created serviceMonitor", "service end ports", serviceMonitor.Spec.Endpoints)
Expect(serviceMonitor.Spec.Selector.MatchLabels).Should(Equal(oamServiceLabel))
Expect(serviceMonitor.Spec.Selector.MatchExpressions).Should(BeNil())
Expect(serviceMonitor.Spec.NamespaceSelector.MatchNames).Should(Equal([]string{metricsTrait.Namespace}))
Expect(serviceMonitor.Spec.NamespaceSelector.Any).Should(BeFalse())
Expect(len(serviceMonitor.Spec.Endpoints)).Should(Equal(1))
Expect(serviceMonitor.Spec.Endpoints[0].Port).Should(BeEmpty())
Expect(*serviceMonitor.Spec.Endpoints[0].TargetPort).Should(BeEquivalentTo(targetPort))
Expect(serviceMonitor.Spec.Endpoints[0].Scheme).Should(Equal(scheme))
Expect(serviceMonitor.Spec.Endpoints[0].Path).Should(Equal(metricsPath))
})
})

View File

@@ -0,0 +1,129 @@
/*
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 metrics
import (
"context"
"path/filepath"
"testing"
monitoringv1 "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1"
oamCore "github.com/crossplane/oam-kubernetes-runtime/apis/core"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
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"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
standardv1alpha1 "github.com/oam-dev/catalog/traits/metricstrait/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 controllerDone chan struct{}
var serviceMonitorNS corev1.Namespace
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
serviceMonitorNS = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: serviceMonitorNSName,
},
}
By("Bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "config", "crd", "bases"),
filepath.Join("..", "hack/crds"), // this has all the required CRDs, a bit hacky
},
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
err = standardv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = monitoringv1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = oamCore.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
By("Create the k8s client")
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
By("Starting the metrics trait controller in the background")
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
MetricsBindAddress: "0",
Port: 9443,
LeaderElection: false,
LeaderElectionID: "9f6dad5a.oam.dev",
})
Expect(err).ToNot(HaveOccurred())
r := MetricsTraitReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("MetricsTrait"),
Scheme: mgr.GetScheme(),
}
Expect(r.SetupWithManager(mgr)).ToNot(HaveOccurred())
controllerDone = make(chan struct{}, 1)
// +kubebuilder:scaffold:builder
go func() {
Expect(mgr.Start(controllerDone)).ToNot(HaveOccurred())
}()
By("Create the serviceMonitor namespace")
Expect(k8sClient.Create(context.Background(), &serviceMonitorNS)).ToNot(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
By("Stop the metricTrait controller")
close(controllerDone)
By("Delete the serviceMonitor namespace")
Expect(k8sClient.Delete(context.Background(), &serviceMonitorNS,
client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed())
By("Tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

25
pkg/utils/json.go Normal file
View File

@@ -0,0 +1,25 @@
/*
Copyright 2019 The Kruise 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 utils
import "encoding/json"
// DumpJSON returns the JSON encoding
func DumpJSON(o interface{}) string {
j, _ := json.Marshal(o)
return string(j)
}

View File

@@ -0,0 +1,13 @@
package metrics_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestMetrics(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Metrics Suite")
}

View File

@@ -0,0 +1,79 @@
package metrics_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/utils/pointer"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/cloud-native-application/rudrx/api/v1alpha1"
. "github.com/cloud-native-application/rudrx/pkg/webhook/metrics"
)
var _ = Describe("Metrics Admission controller Test", func() {
var traitBase v1alpha1.MetricsTrait
BeforeEach(func() {
traitBase = v1alpha1.MetricsTrait{
ObjectMeta: metav1.ObjectMeta{
Name: "mutate-hook",
},
Spec: v1alpha1.MetricsTraitSpec{
ScrapeService: v1alpha1.ScapeServiceEndPoint{
TargetPort: intstr.FromInt(1234),
},
},
}
})
It("Test with fill in all default", func() {
trait := traitBase
want := traitBase
want.Spec.ScrapeService.Format = SupportedFormat
want.Spec.ScrapeService.Scheme = SupportedScheme
want.Spec.ScrapeService.Path = DefaultMetricsPath
want.Spec.ScrapeService.Enabled = pointer.BoolPtr(false)
Default(&trait)
Expect(trait).Should(BeEquivalentTo(want))
})
It("Test only fill in empty fields", func() {
trait := traitBase
trait.Spec.ScrapeService.Path = "not default"
want := trait
want.Spec.ScrapeService.Format = SupportedFormat
want.Spec.ScrapeService.Scheme = SupportedScheme
want.Spec.ScrapeService.Enabled = pointer.BoolPtr(true)
Default(&trait)
Expect(trait).Should(BeEquivalentTo(want))
})
It("Test not fill in enabled field", func() {
trait := traitBase
trait.Spec.ScrapeService.Enabled = &falseVar
want := trait
want.Spec.ScrapeService.Format = SupportedFormat
want.Spec.ScrapeService.Scheme = SupportedScheme
want.Spec.ScrapeService.Path = DefaultMetricsPath
want.Spec.ScrapeService.Enabled = &falseVar
Default(&trait)
Expect(trait).Should(BeEquivalentTo(want))
})
It("Test validate valid trait", func() {
trait := traitBase
trait.Spec.ScrapeService.Format = SupportedFormat
trait.Spec.ScrapeService.Scheme = SupportedScheme
Expect(ValidateCreate(&trait).ToAggregate()).NotTo(HaveOccurred())
Expect(ValidateUpdate(&trait, nil).ToAggregate()).NotTo(HaveOccurred())
Expect(ValidateDelete(&trait).ToAggregate()).NotTo(HaveOccurred())
})
It("Test validate invalid trait", func() {
trait := traitBase
Expect(ValidateCreate(&trait).ToAggregate()).To(HaveOccurred())
Expect(ValidateUpdate(&trait, nil).ToAggregate()).To(HaveOccurred())
})
})

View File

@@ -0,0 +1,115 @@
/*
Copyright 2019 The Kruise 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 metrics
import (
"context"
"encoding/json"
"net/http"
"k8s.io/klog"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/cloud-native-application/rudrx/api/v1alpha1"
util "github.com/cloud-native-application/rudrx/pkg/utils"
)
const (
// SupportedFormat is the only metrics data format we support
SupportedFormat = "prometheus"
// SupportedScheme is the only scheme we support
SupportedScheme = "http"
// DefaultMetricsPath is the default metrics path we support
DefaultMetricsPath = "/metrics"
)
// CloneSetCreateUpdateHandler handles CloneSet
type MetricsTraitMutatingHandler struct {
Client client.Client
// Decoder decodes objects
Decoder *admission.Decoder
}
// log is for logging in this package.
var mutatelog = logf.Log.WithName("metricstrait-mutate")
var _ admission.Handler = &MetricsTraitMutatingHandler{}
// Handle handles admission requests.
func (h *MetricsTraitMutatingHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
obj := &v1alpha1.MetricsTrait{}
err := h.Decoder.Decode(req, obj)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
Default(obj)
marshalled, err := json.Marshal(obj)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
resp := admission.PatchResponseFromRaw(req.AdmissionRequest.Object.Raw, marshalled)
if len(resp.Patches) > 0 {
klog.V(5).Infof("Admit MetricsTrait %s/%s patches: %v", obj.Namespace, obj.Name, util.DumpJSON(resp.Patches))
}
return resp
}
// Default sets all the default value for the metricsTrait
func Default(obj *v1alpha1.MetricsTrait) {
mutatelog.Info("default", "name", obj.Name)
if len(obj.Spec.ScrapeService.Format) == 0 {
mutatelog.Info("default format as prometheus")
obj.Spec.ScrapeService.Format = SupportedFormat
}
if len(obj.Spec.ScrapeService.Path) == 0 {
mutatelog.Info("default path as /metrics")
obj.Spec.ScrapeService.Path = DefaultMetricsPath
}
if len(obj.Spec.ScrapeService.Scheme) == 0 {
mutatelog.Info("default scheme as http")
obj.Spec.ScrapeService.Scheme = SupportedScheme
}
if obj.Spec.ScrapeService.Enabled == nil {
mutatelog.Info("default enabled as true")
obj.Spec.ScrapeService.Enabled = pointer.BoolPtr(true)
}
}
var _ inject.Client = &MetricsTraitMutatingHandler{}
// InjectClient injects the client into the MetricsTraitMutatingHandler
func (h *MetricsTraitMutatingHandler) InjectClient(c client.Client) error {
h.Client = c
return nil
}
var _ admission.DecoderInjector = &MetricsTraitMutatingHandler{}
// InjectDecoder injects the decoder into the MetricsTraitMutatingHandler
func (h *MetricsTraitMutatingHandler) InjectDecoder(d *admission.Decoder) error {
h.Decoder = d
return nil
}

View File

@@ -0,0 +1,119 @@
/*
Copyright 2019 The Kruise 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 metrics
import (
"context"
"fmt"
"net/http"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/cloud-native-application/rudrx/api/v1alpha1"
)
// MetricsTraitValidatingHandler handles MetricsTrait
type MetricsTraitValidatingHandler struct {
Client client.Client
// Decoder decodes objects
Decoder *admission.Decoder
}
// log is for logging in this package.
var validatelog = logf.Log.WithName("metricstrait-validate")
var _ admission.Handler = &MetricsTraitValidatingHandler{}
// Handle handles admission requests.
func (h *MetricsTraitValidatingHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
obj := &v1alpha1.MetricsTrait{}
err := h.Decoder.Decode(req, obj)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
switch req.AdmissionRequest.Operation {
case admissionv1beta1.Create:
if allErrs := ValidateCreate(obj); len(allErrs) > 0 {
return admission.Errored(http.StatusUnprocessableEntity, allErrs.ToAggregate())
}
case admissionv1beta1.Update:
oldObj := &v1alpha1.MetricsTrait{}
if err := h.Decoder.DecodeRaw(req.AdmissionRequest.OldObject, oldObj); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if allErrs := ValidateUpdate(obj, oldObj); len(allErrs) > 0 {
return admission.Errored(http.StatusUnprocessableEntity, allErrs.ToAggregate())
}
}
return admission.ValidationResponse(true, "")
}
// ValidateCreate validates the metricsTrait on creation
func ValidateCreate(r *v1alpha1.MetricsTrait) field.ErrorList {
validatelog.Info("validate create", "name", r.Name)
allErrs := apivalidation.ValidateObjectMeta(&r.ObjectMeta, true, apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
fldPath := field.NewPath("metadata")
if r.Spec.ScrapeService.Format != SupportedFormat {
allErrs = append(allErrs, field.Invalid(fldPath.Child("ScrapeService.Format"), r.Spec.ScrapeService.Format,
fmt.Sprintf("the data format `%s` is not supported", r.Spec.ScrapeService.Format)))
}
if r.Spec.ScrapeService.Scheme != SupportedScheme {
allErrs = append(allErrs, field.Invalid(fldPath.Child("ScrapeService.Format"), r.Spec.ScrapeService.Scheme,
fmt.Sprintf("the scheme `%s` is not supported", r.Spec.ScrapeService.Scheme)))
}
return allErrs
}
// ValidateUpdate validates the metricsTrait on update
func ValidateUpdate(r *v1alpha1.MetricsTrait, _ *v1alpha1.MetricsTrait) field.ErrorList {
validatelog.Info("validate update", "name", r.Name)
return ValidateCreate(r)
}
// ValidateDelete validates the metricsTrait on delete
func ValidateDelete(r *v1alpha1.MetricsTrait) field.ErrorList {
validatelog.Info("validate delete", "name", r.Name)
return nil
}
var _ inject.Client = &MetricsTraitValidatingHandler{}
// InjectClient injects the client into the MetricsTraitValidatingHandler
func (h *MetricsTraitValidatingHandler) InjectClient(c client.Client) error {
h.Client = c
return nil
}
var _ admission.DecoderInjector = &MetricsTraitValidatingHandler{}
// InjectDecoder injects the decoder into the MetricsTraitValidatingHandler
func (h *MetricsTraitValidatingHandler) InjectDecoder(d *admission.Decoder) error {
h.Decoder = d
return nil
}

20
pkg/webhook/register.go Normal file
View File

@@ -0,0 +1,20 @@
package webhook
import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"github.com/cloud-native-application/rudrx/pkg/webhook/metrics"
)
// +kubebuilder:webhook:verbs=create;update;delete,path=/validate-standard-oam-dev-v1alpha1-metricstrait,mutating=false,failurePolicy=fail,groups=standard.oam.dev,resources=metricstraits,versions=v1alpha1,name=vmetricstrait.kb.io
// +kubebuilder:webhook:path=/mutate-standard-oam-dev-v1alpha1-metricstrait,mutating=true,failurePolicy=fail,groups=standard.oam.dev,resources=metricstraits,verbs=create;update,versions=v1alpha1,name=mmetricstrait.kb.io
// Register will register all the services to the webhook server
func Register(mgr manager.Manager) {
server := mgr.GetWebhookServer()
server.Register("/validate-standard-oam-dev-v1alpha1-metricstrait",
&webhook.Admission{Handler: &metrics.MetricsTraitValidatingHandler{}})
server.Register("/mutate-standard-oam-dev-v1alpha1-metricstrait",
&webhook.Admission{Handler: &metrics.MetricsTraitMutatingHandler{}})
}