Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
71 changes: 67 additions & 4 deletions cmd/cluster-samples-operator/main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package main

import (
"context"
"fmt"
"os"
"runtime"
"time"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

kubeyaml "k8s.io/apimachinery/pkg/util/yaml"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"

v1 "github.com/openshift/api/config/v1"
"github.com/openshift/library-go/pkg/operator/watchdog"

"github.com/openshift/cluster-samples-operator/pkg/metrics"
Expand All @@ -27,28 +32,86 @@ func main() {
Short: "OpenShift cluster samples operator",
Run: runOperator,
}
cmd.Flags().String("config", "", "Path to the controller config file")
cmd.AddCommand(watchdog.NewFileWatcherWatchdog())
if err := cmd.Execute(); err != nil {
logrus.Errorf("%v", err)
os.Exit(1)
}
}

// readAndParseControllerConfig reads and parses the controller configuration file.
// If path is empty, returns default config (for backwards compatibility during
// migration to file-based configuration).
func readAndParseControllerConfig(path string) (v1.GenericControllerConfig, error) {
config := v1.GenericControllerConfig{
ServingInfo: v1.HTTPServingInfo{
ServingInfo: v1.ServingInfo{
BindAddress: fmt.Sprintf(":%d", metrics.MetricsPort),
},
},
}
if path == "" {
return config, nil
}

content, err := os.ReadFile(path)
if err != nil {
return config, fmt.Errorf("failed to read config file: %w", err)
}

if err := kubeyaml.Unmarshal(content, &config); err != nil {
return config, fmt.Errorf("failed to unmarshal config content: %w", err)
}

// make sure we always have a bind address present in the config. This
// is just for the case where the config has an explicitly empty
// bindAddress value.
if config.ServingInfo.BindAddress == "" {
config.ServingInfo.BindAddress = fmt.Sprintf(":%d", metrics.MetricsPort)
}

return config, nil
}

func runOperator(cmd *cobra.Command, args []string) {
printVersion()

// set up signals so we handle the first shutdown signal gracefully
stopCh := signals.SetupSignalHandler()

ctrlConfig, err := cmd.Flags().GetString("config")
if err != nil {
logrus.Fatal(err)
}

config, err := readAndParseControllerConfig(ctrlConfig)
if err != nil {
logrus.Fatal(err)
}

for _, arg := range args {
if arg == "-v" {
logrus.SetLevel(logrus.DebugLevel)
break
}
}

// set up signals so we handle the first shutdown signal gracefully
stopCh := signals.SetupSignalHandler()
srv, err := metrics.NewServer(config)
if err != nil {
logrus.Fatal(err)
}

srv := metrics.BuildServer(metrics.MetricsPort)
go metrics.RunServer(srv, stopCh)
if err := srv.Start(); err != nil {
logrus.Fatal(err)
}
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := srv.Stop(ctx); err != nil {
logrus.Error(err)
}
cancel()
}()

controller, err := operator.NewController()
if err != nil {
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module github.com/openshift/cluster-samples-operator
go 1.25.0

require (
github.com/openshift/api v0.0.0-20230804122727-754e59a61dd3
github.com/openshift/build-machinery-go v0.0.0-20211213093930-7e33a7eb4ce3
github.com/openshift/client-go v0.0.0-20201020074620-f8fd44879f7c
github.com/openshift/library-go v0.0.0-20201023142140-a2f8f23f66f0
github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee
github.com/openshift/client-go v0.0.0-20260317180604-743f664b82d1
github.com/openshift/library-go v0.0.0-20260326200317-12d8376369b7
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.66.1
Expand Down Expand Up @@ -54,7 +54,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
Expand Down
637 changes: 10 additions & 627 deletions go.sum

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions manifests/06-operator-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
annotations:
capability.openshift.io/name: openshift-samples
include.release.openshift.io/ibm-cloud-managed: "true"
include.release.openshift.io/self-managed-high-availability: "true"
config.openshift.io/inject-tls: "true"
name: samples-operator-config
namespace: openshift-cluster-samples-operator
data:
config.yaml: |-
apiVersion: config.openshift.io/v1
kind: GenericControllerConfig
servingInfo:
bindAddress: ":60000"
16 changes: 15 additions & 1 deletion manifests/06-operator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ spec:
- name: samples-operator-tls
secret:
secretName: samples-operator-tls
- name: samples-operator-config
configMap:
name: samples-operator-config
containers:
- name: cluster-samples-operator
image: quay.io/openshift/origin-cluster-samples-operator:latest
Expand All @@ -52,6 +55,8 @@ spec:
name: metrics
command:
- cluster-samples-operator
args:
- --config=/var/run/configmaps/samples-operator-config/config.yaml
resources:
requests:
memory: 50Mi
Expand All @@ -66,6 +71,8 @@ spec:
volumeMounts:
- name: samples-operator-tls
mountPath: /etc/secrets
- name: samples-operator-config
mountPath: /var/run/configmaps/samples-operator-config
env:
- name: WATCH_NAMESPACE
valueFrom:
Expand All @@ -84,7 +91,9 @@ spec:
- --namespace=openshift-cluster-samples-operator
- --process-name=cluster-samples-operator
- --termination-grace-period=30s
- --files=/etc/secrets/tls.crt,/etc/secrets/tls.key
- --files=/etc/secrets/tls.crt
- --files=/etc/secrets/tls.key
- --files=/var/run/configmaps/samples-operator-config/config.yaml
imagePullPolicy: IfNotPresent
terminationMessagePolicy: FallbackToLogsOnError
resources:
Expand All @@ -96,3 +105,8 @@ spec:
capabilities:
drop:
- ALL
volumeMounts:
- name: samples-operator-tls
mountPath: /etc/secrets
- name: samples-operator-config
mountPath: /var/run/configmaps/samples-operator-config
103 changes: 77 additions & 26 deletions pkg/metrics/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package metrics

import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"time"

v1 "github.com/openshift/api/config/v1"
"github.com/openshift/library-go/pkg/crypto"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
Expand All @@ -16,45 +19,93 @@ var (
tlsKey = "/etc/secrets/tls.key"
)

// BuildServer creates the http.Server struct
func BuildServer(port int) *http.Server {
if port <= 0 {
logrus.Error("invalid port for metric server")
return nil
// MetricsServer controls a http server capable of serving prometheus metrics
// through https connections. A metric server is configured by means of a
// v1.GenericControllerConfig object.
type MetricsServer struct {
bindAddress string
tlsConfig *tls.Config
httpServer *http.Server
}

// NewServer returns a new metrics server configured according to the provided
// GenericControllerConfig. If no bind port is configured then the http
// server will listen on MetricsPort constant. If the configuration
// does not provide certificate paths then tlsCRT and tlsKey variables
// are used as default location.
func NewServer(cfg v1.GenericControllerConfig) (*MetricsServer, error) {
bindAddress := fmt.Sprintf(":%d", MetricsPort)
if len(cfg.ServingInfo.BindAddress) != 0 {
bindAddress = cfg.ServingInfo.BindAddress
}

minTLS, err := crypto.TLSVersion(cfg.ServingInfo.MinTLSVersion)
if err != nil {
return nil, fmt.Errorf("invalid min tls version: %w", err)
}

var suites []uint16
for _, suiteString := range cfg.ServingInfo.CipherSuites {
suite, err := crypto.CipherSuite(suiteString)
if err != nil {
return nil, fmt.Errorf("invalid cipher suite: %w", err)
}
suites = append(suites, suite)
}

bindAddr := fmt.Sprintf(":%d", port)
router := http.NewServeMux()
router.Handle("/metrics", promhttp.Handler())
srv := &http.Server{
Addr: bindAddr,
Handler: router,

crt, key := tlsCRT, tlsKey
if len(cfg.ServingInfo.CertFile) != 0 {
crt = cfg.ServingInfo.CertFile
}
if len(cfg.ServingInfo.KeyFile) != 0 {
key = cfg.ServingInfo.KeyFile
}

certificate, err := tls.LoadX509KeyPair(crt, key)
if err != nil {
return nil, fmt.Errorf("failed to load certificates: %w", err)
}

return srv
return &MetricsServer{
bindAddress: bindAddress,
httpServer: &http.Server{Handler: router},
tlsConfig: &tls.Config{
CipherSuites: suites,
MinVersion: minTLS,
Certificates: []tls.Certificate{certificate},
},
}, nil
}

// StopServer stops the server; for tls secret rotation
func StopServer(srv *http.Server) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logrus.Warningf("Problem shutting down HTTP server: %s", err.Error())
// Start starts the metrics server on a go routine. Fails during bind stage
// are immediately returned.
func (m *MetricsServer) Start() error {
listener, err := net.Listen("tcp", m.bindAddress)
if err != nil {
return fmt.Errorf("failed to listen: %w", err)
}
}

// RunServer starts the metrics server.
func RunServer(srv *http.Server, stopCh <-chan struct{}) {
go func() {
err := srv.ListenAndServeTLS(tlsCRT, tlsKey)
if err != nil && err != http.ErrServerClosed {
logrus.Errorf("error starting metrics server: %v", err)
if err := m.httpServer.Serve(
tls.NewListener(listener, m.tlsConfig),
); err != nil && err != http.ErrServerClosed {
logrus.Errorf("failed to serve metrics: %s", err)
}
}()
<-stopCh
if err := srv.Close(); err != nil {
logrus.Errorf("error closing metrics server: %v", err)

return nil
}

// Stop stops the underlying http server. This function waits for the server to
// gracefully exit (it might take a while).
func (m *MetricsServer) Stop(ctx context.Context) error {
if err := m.httpServer.Shutdown(ctx); err != nil {
return fmt.Errorf("failed to shutdown http server: %w", err)
}
return nil
}

// Degraded sets the metric that indicates if the operator is in degraded
Expand Down
Loading