diff --git a/Gopkg.lock b/Gopkg.lock index 60c7b824df..a3c37e68cb 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -13,6 +13,12 @@ packages = ["."] revision = "de5bf2ad457846296e2031421a34e2568e304e35" +[[projects]] + branch = "master" + name = "github.com/beorn7/perks" + packages = ["quantile"] + revision = "3a771d992973f24aa725d07868b467d1ddfceafb" + [[projects]] name = "github.com/davecgh/go-spew" packages = ["spew"] @@ -165,6 +171,12 @@ ] revision = "32fa128f234d041f196a9f3e0fea5ac9772c08e1" +[[projects]] + name = "github.com/matttproud/golang_protobuf_extensions" + packages = ["pbutil"] + revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" + version = "v1.0.1" + [[projects]] branch = "master" name = "github.com/petar/GoLLRB" @@ -177,6 +189,42 @@ revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" version = "v2.0.1" +[[projects]] + name = "github.com/prometheus/client_golang" + packages = [ + "prometheus", + "prometheus/promhttp" + ] + revision = "c5b7fccd204277076155f10851dad72b76a49317" + version = "v0.8.0" + +[[projects]] + branch = "master" + name = "github.com/prometheus/client_model" + packages = ["go"] + revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" + +[[projects]] + branch = "master" + name = "github.com/prometheus/common" + packages = [ + "expfmt", + "internal/bitbucket.org/ww/goautoneg", + "model" + ] + revision = "7600349dcfe1abd18d72d3a1770870d9800a7801" + +[[projects]] + branch = "master" + name = "github.com/prometheus/procfs" + packages = [ + ".", + "internal/util", + "nfs", + "xfs" + ] + revision = "7d6f385de8bea29190f15ba9931442a0eaef9af7" + [[projects]] name = "github.com/sergi/go-diff" packages = ["diffmatchpatch"] @@ -410,6 +458,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "d7e4cf2bf82190094599179d028dadf57d65b437bed4e73be65d1f823ee21e60" + inputs-digest = "e39a3b50eecf50ee2f3c6ce8a36306abeea762a41fab1117f0c5e2a038b72fb4" solver-name = "gps-cdcl" solver-version = 1 diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index e00118672c..ed3758bed2 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -23,6 +23,8 @@ import ( "path/filepath" "strings" "text/template" + + k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" ) const ( @@ -237,8 +239,11 @@ func renderDeployFiles(deployDir, projectName, apiVersion, kind string) error { // RenderOperatorYaml generates "deploy/operator.yaml" func RenderOperatorYaml(c *Config, image string) error { td := tmplData{ - ProjectName: c.ProjectName, - Image: image, + ProjectName: c.ProjectName, + Image: image, + MetricsPort: k8sutil.PrometheusMetricsPort, + MetricsPortName: k8sutil.PrometheusMetricsPortName, + OperatorNameEnv: k8sutil.OperatorNameEnvVar, } return renderWriteFile(operatorYaml, operatorTmplName, operatorYamlTmpl, td) } @@ -443,8 +448,11 @@ type tmplData struct { // plural name to be used in the URL: /apis/// KindPlural string - Image string - Name string + Image string + Name string + MetricsPort int + MetricsPortName string + OperatorNameEnv string PackageName string ChannelName string diff --git a/pkg/generator/generator_test.go b/pkg/generator/generator_test.go index 83712ce231..b3f72e507c 100644 --- a/pkg/generator/generator_test.go +++ b/pkg/generator/generator_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" "github.com/sergi/go-diff/diffmatchpatch" ) @@ -199,6 +200,9 @@ spec: containers: - name: app-operator image: quay.io/example-inc/app-operator:0.0.1 + ports: + - containerPort: 60000 + name: metrics command: - app-operator imagePullPolicy: Always @@ -207,6 +211,8 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + - name: OPERATOR_NAME + value: "app-operator" ` const rbacYamlExp = `kind: Role @@ -276,7 +282,14 @@ func TestGenDeploy(t *testing.T) { } buf = &bytes.Buffer{} - if err := renderFile(buf, operatorTmplName, operatorYamlTmpl, tmplData{ProjectName: appProjectName, Image: appImage}); err != nil { + td := tmplData{ + ProjectName: appProjectName, + Image: appImage, + MetricsPort: k8sutil.PrometheusMetricsPort, + MetricsPortName: k8sutil.PrometheusMetricsPortName, + OperatorNameEnv: k8sutil.OperatorNameEnvVar, + } + if err := renderFile(buf, operatorTmplName, operatorYamlTmpl, td); err != nil { t.Error(err) } if operatorYamlExp != buf.String() { @@ -423,6 +436,8 @@ func printVersion() { func main() { printVersion() + sdk.ExposeMetricsPort() + resource := "app.example.com/v1alpha1" kind := "AppService" namespace, err := k8sutil.GetWatchNamespace() diff --git a/pkg/generator/templates.go b/pkg/generator/templates.go index 41e0ffaf7e..44a4ce5777 100644 --- a/pkg/generator/templates.go +++ b/pkg/generator/templates.go @@ -150,6 +150,8 @@ func printVersion() { func main() { printVersion() + sdk.ExposeMetricsPort() + resource := "{{.APIVersion}}" kind := "{{.Kind}}" namespace, err := k8sutil.GetWatchNamespace() @@ -275,6 +277,7 @@ required = [ branch = "master" # version = "=v0.0.5" ` + const projectGitignoreTmpl = ` # Temporary Build Files tmp/_output @@ -417,6 +420,9 @@ spec: containers: - name: {{.ProjectName}} image: {{.Image}} + ports: + - containerPort: {{.MetricsPort}} + name: {{.MetricsPortName}} command: - {{.ProjectName}} imagePullPolicy: Always @@ -425,6 +431,8 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace + - name: {{.OperatorNameEnv}} + value: "{{.ProjectName}}" ` const rbacYamlTmpl = `kind: Role diff --git a/pkg/sdk/metrics.go b/pkg/sdk/metrics.go new file mode 100644 index 0000000000..7c8b521192 --- /dev/null +++ b/pkg/sdk/metrics.go @@ -0,0 +1,28 @@ +package sdk + +import ( + "net/http" + "strconv" + + k8sutil "github.com/operator-framework/operator-sdk/pkg/util/k8sutil" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/api/errors" +) + +// ExposeMetricsPort generate a Kubernetes Service to expose metrics port +func ExposeMetricsPort() { + http.Handle("/"+k8sutil.PrometheusMetricsPortName, promhttp.Handler()) + go http.ListenAndServe(":"+strconv.Itoa(k8sutil.PrometheusMetricsPort), nil) + + service, err := k8sutil.InitOperatorService() + if err != nil { + logrus.Fatalf("Failed to init operator service: %v", err) + } + err = Create(service) + if err != nil && !errors.IsAlreadyExists(err) { + logrus.Infof("Failed to create operator service: %v", err) + return + } + logrus.Infof("Metrics service %s created", service.Name) +} diff --git a/pkg/util/k8sutil/constants.go b/pkg/util/k8sutil/constants.go index fd67036cca..12f8d6063c 100644 --- a/pkg/util/k8sutil/constants.go +++ b/pkg/util/k8sutil/constants.go @@ -8,4 +8,14 @@ const ( // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE // which is the namespace that the pod is currently running in. WatchNamespaceEnvVar = "WATCH_NAMESPACE" + + // OperatorNameEnvVar is the constant for env variable OPERATOR_NAME + // wich is the name of the current operator + OperatorNameEnvVar = "OPERATOR_NAME" + + // PrometheusMetricsPort defines the port which expose prometheus metrics + PrometheusMetricsPort = 60000 + + // PrometheusMetricsPortName define the port name used in kubernetes deployment and service + PrometheusMetricsPortName = "metrics" ) diff --git a/pkg/util/k8sutil/k8sutil.go b/pkg/util/k8sutil/k8sutil.go index f317f67bb5..ae8eb143f5 100644 --- a/pkg/util/k8sutil/k8sutil.go +++ b/pkg/util/k8sutil/k8sutil.go @@ -19,12 +19,14 @@ import ( "fmt" "os" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" 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/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + intstr "k8s.io/apimachinery/pkg/util/intstr" cgoscheme "k8s.io/client-go/kubernetes/scheme" ) @@ -147,3 +149,53 @@ func GetWatchNamespace() (string, error) { } return ns, nil } + +// GetOperatorName return the operator name +func GetOperatorName() (string, error) { + operatorName, found := os.LookupEnv(OperatorNameEnvVar) + if !found { + return "", fmt.Errorf("%s must be set", OperatorNameEnvVar) + } + if len(operatorName) == 0 { + return "", fmt.Errorf("%s must not be empty", OperatorNameEnvVar) + } + return operatorName, nil +} + +// InitOperatorService return the static service which expose operator metrics +func InitOperatorService() (*v1.Service, error) { + operatorName, err := GetOperatorName() + if err != nil { + return nil, err + } + namespace, err := GetWatchNamespace() + if err != nil { + return nil, err + } + service := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: operatorName, + Namespace: namespace, + Labels: map[string]string{"name": operatorName}, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Port: PrometheusMetricsPort, + Protocol: v1.ProtocolTCP, + TargetPort: intstr.IntOrString{ + Type: intstr.String, + StrVal: PrometheusMetricsPortName, + }, + Name: PrometheusMetricsPortName, + }, + }, + Selector: map[string]string{"name": operatorName}, + }, + } + return service, nil +} diff --git a/pkg/util/k8sutil/k8sutil_test.go b/pkg/util/k8sutil/k8sutil_test.go new file mode 100644 index 0000000000..0e5c257874 --- /dev/null +++ b/pkg/util/k8sutil/k8sutil_test.go @@ -0,0 +1,61 @@ +package k8sutil + +import ( + "fmt" + "os" + "reflect" + "testing" +) + +func TestGetOperatorName(t *testing.T) { + type Output struct { + operatorName string + err error + } + + type Scenario struct { + name string + envVarKey string + envVarValue string + expectedOutput Output + } + + tests := []Scenario{ + Scenario{ + name: "Simple case", + envVarKey: OperatorNameEnvVar, + envVarValue: "myoperator", + expectedOutput: Output{ + operatorName: "myoperator", + err: nil, + }, + }, + Scenario{ + name: "Unset env var", + envVarKey: "", + envVarValue: "", + expectedOutput: Output{ + operatorName: "", + err: fmt.Errorf("%s must be set", OperatorNameEnvVar), + }, + }, + Scenario{ + name: "Empty env var", + envVarKey: OperatorNameEnvVar, + envVarValue: "", + expectedOutput: Output{ + operatorName: "", + err: fmt.Errorf("%s must not be empty", OperatorNameEnvVar), + }, + }, + } + + for _, test := range tests { + _ = os.Setenv(test.envVarKey, test.envVarValue) + operatorName, err := GetOperatorName() + if !(operatorName == test.expectedOutput.operatorName && reflect.DeepEqual(err, test.expectedOutput.err)) { + t.Errorf("test %s failed, expected ouput: %s,%v; got: %s,%v", test.name, test.expectedOutput.operatorName, test.expectedOutput.err, operatorName, err) + } + _ = os.Unsetenv(test.envVarKey) + } +}